From 0cfcfaf8994528814bfb791c2724548479ea51c2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 1 Jul 2025 20:38:18 -0400 Subject: [PATCH 001/185] 3rd large migration to outbox. --- .idea/deviceManager.xml | 13 + .idea/kotlinNotebook.xml | 6 + .idea/kotlinc.xml | 6 + README.md | 2 +- .../com/vitorpamplona/amethyst/Amethyst.kt | 32 +- .../com/vitorpamplona/amethyst/DebugUtils.kt | 6 +- .../amethyst/LocalPreferences.kt | 68 +- .../vitorpamplona/amethyst/ServiceManager.kt | 142 - .../vitorpamplona/amethyst/model/Account.kt | 3077 ++++------------- .../amethyst/model/AccountSettings.kt | 100 +- .../amethyst/model/AntiSpamFilter.kt | 10 +- .../vitorpamplona/amethyst/model/Channel.kt | 48 +- .../vitorpamplona/amethyst/model/Constants.kt | 49 + .../amethyst/model/HashtagIcon.kt | 4 +- .../amethyst/model/LocalCache.kt | 554 +-- .../com/vitorpamplona/amethyst/model/Note.kt | 59 +- .../com/vitorpamplona/amethyst/model/User.kt | 30 +- .../edits/PrivateStorageRelayListState.kt | 117 + .../model/emphChat/EphemeralChatListState.kt | 69 +- .../model/localRelays/LocalRelayListState.kt | 62 + .../AccountOutboxRelayState.kt | 55 + .../NotificationInboxRelayState.kt} | 45 +- .../nip01UserMetadata/UserMetadataState.kt | 124 + .../FollowListOutboxRelays.kt | 100 + .../model/nip02FollowLists/FollowListState.kt | 171 + .../nip02FollowLists/FollowsPerOutboxRelay.kt | 94 + .../model/nip17Dms/DmInboxRelayState.kt | 59 + .../model/nip17Dms/DmRelayListState.kt | 114 + .../model/nip18Reposts/RepostAction.kt | 57 + .../model/nip25Reactions/ReactionAction.kt | 109 + .../nip28PublicChats/PublicChatListState.kt | 76 +- .../model/nip30CustomEmojis/EmojiPackState.kt | 41 +- .../nip38UserStatuses/UserStatusAction.kt | 71 + .../model/nip50Search/SearchRelayListState.kt | 114 + .../model/nip51Lists/BlockPeopleListState.kt | 158 + .../model/nip51Lists/GeohashListState.kt | 150 + .../model/nip51Lists/HashtagListState.kt | 150 + .../model/nip51Lists/HiddenUsersState.kt | 116 + .../model/nip51Lists/MuteListState.kt | 185 + .../model/nip56Reports/ReportAction.kt | 71 + .../nip65RelayList/Nip65RelayListState.kt | 149 + .../nip65RelayList/OutboxRelaySetState.kt | 103 + .../nip72Communities/CommunityListState.kt | 177 + .../nip78AppSpecific/AppSpecificState.kt | 111 + .../FileStorageServerListState.kt | 84 + .../nipB7Blossom/BlossomServerListState.kt | 91 + .../observables/LatestByKindAndAuthor.kt | 7 +- .../serverList/MergedFollowListsState.kt | 84 + .../MergedFollowPlusMineRelayListsState.kt | 86 + .../model/serverList/MergedServerListState.kt | 74 + .../serverList/TrustedRelayListsState.kt | 88 + .../model/topNavFeeds/CommunityRelayLoader.kt | 81 + .../topNavFeeds/FeedTopNavFilterState.kt | 86 + .../topNavFeeds/IFeedFlowsType.kt} | 15 +- .../model/topNavFeeds/IFeedTopNavFilter.kt | 22 +- .../topNavFeeds/IFeedTopNavPerRelayFilter.kt | 23 + .../IFeedTopNavPerRelayFilterSet.kt | 23 + .../MergedTopFeedAuthorListsState.kt | 112 + .../model/topNavFeeds/OutboxLoaderState.kt | 53 + .../model/topNavFeeds/OutboxRelayLoader.kt | 80 + .../AllFollowsByOutboxTopNavFilter.kt | 140 + ...AllFollowsByOutboxTopNavPerRelayFilter.kt} | 28 +- ...lFollowsByOutboxTopNavPerRelayFilterSet.kt | 28 + .../allFollows/AllFollowsFeedFlow.kt | 57 + .../topNavFeeds/aroundMe/AroundMeExpander.kt | 63 + .../topNavFeeds/aroundMe/AroundMeFeedFlow.kt | 56 + .../aroundMe/LocationTopNavFilter.kt | 65 + .../aroundMe/LocationTopNavPerRelayFilter.kt | 32 + .../LocationTopNavPerRelayFilterSet.kt | 28 + .../topNavFeeds/global/GlobalFeedFlow.kt | 34 +- .../topNavFeeds/global/GlobalTopNavFilter.kt | 52 + .../global/GlobalTopNavPerRelayFilter.kt | 14 +- .../global/GlobalTopNavPerRelayFilterSet.kt | 28 + .../hashtag/HashtagTopNavFilter.kt | 68 + .../hashtag/HashtagTopNavPerRelayFilter.kt | 24 +- .../hashtag/HashtagTopNavPerRelayFilterSet.kt | 28 + .../topNavFeeds/noteBased/NoteFeedFlow.kt | 131 + .../AllCommunitiesTopNavFilter.kt | 59 + .../AllCommunitiesTopNavPerRelayFilter.kt | 27 + .../AllCommunitiesTopNavPerRelayFilterSet.kt | 28 + .../author/AuthorsByOutboxTopNavFilter.kt | 59 + .../AuthorsByOutboxTopNavPerRelayFilter.kt | 29 + .../AuthorsByOutboxTopNavPerRelayFilterSet.kt | 28 + .../community/SingleCommunityTopNavFilter.kt | 115 + .../SingleCommunityTopNavPerRelayFilter.kt | 30 + .../SingleCommunityTopNavPerRelayFilterSet.kt | 28 + .../muted/MutedAuthorsByOutboxTopNavFilter.kt | 59 + ...utedAuthorsByOutboxTopNavPerRelayFilter.kt | 29 + ...dAuthorsByOutboxTopNavPerRelayFilterSet.kt | 28 + .../topNavFeeds/unknown/UnknownFeedFlow.kt | 37 + .../unknown/UnknownTopNavFilter.kt} | 32 +- .../unknown/UnknownTopNavPerRelayFilterSet.kt | 13 +- .../model/torState/TorRelayEvaluation.kt | 50 + .../model/torState/TorRelaySettings.kt | 31 + .../amethyst/model/torState/TorRelayState.kt | 107 + .../service/Nip11RelayInfoRetriever.kt | 65 +- .../amethyst/service/ZapPaymentHandler.kt | 44 +- .../service/notifications/RegisterAccounts.kt | 13 +- .../service/okhttp/DualHttpClientManager.kt | 4 +- .../service/okhttp/OkHttpWebSocket.kt | 59 +- .../ProxySettingsAnchor.kt} | 42 +- .../relayClient/CacheClientConnector.kt | 16 +- .../service/relayClient/RelayLogger.kt | 15 +- .../relayClient/RelayProxyClientConnector.kt | 71 + .../service/relayClient/RelaySpeedLogger.kt | 23 +- .../authCommand/model/AuthCoordinator.kt | 4 +- .../ComposeSubscriptionManager.kt | 4 - .../eoseManagers/BaseEoseManager.kt | 2 +- .../eoseManagers/PerUniqueIdEoseManager.kt | 17 +- .../PerUserAndFollowListEoseManager.kt | 19 +- .../eoseManagers/PerUserEoseManager.kt | 19 +- .../eoseManagers/SingleSubEoseManager.kt | 19 +- .../SingleSubNoEoseCacheEoseManager.kt | 8 +- .../compose/DisplayNotifyMessages.kt | 15 +- .../notifyCommand/model/NotifyCoordinator.kt | 4 +- .../notifyCommand/model/NotifyRequest.kt | 4 +- .../model/NotifyRequestsCache.kt | 5 +- .../RelaySubscriptionsCoordinator.kt | 4 +- .../account/AccountFilterAssembler.kt | 2 +- .../AccountFilterAssemblerSubscription.kt | 2 +- .../reqCommand/account/AccountObservers.kt | 4 +- .../metadata/AccountMetadataEoseManager.kt | 63 +- .../FilterAccountInfoAndListsFromKey.kt | 65 +- .../FilterBasicAccountInfoFromKeys.kt | 44 +- .../metadata/FilterDraftsAndReportsFromKey.kt | 34 +- .../metadata/FilterFollowsAndMutesFromKey.kt | 44 +- .../metadata/FilterLastPostsFromKey.kt | 22 +- .../AccountNotificationsEoseManager.kt | 47 +- .../FilterNotificationsToPubkey.kt | 92 +- .../AccountGiftWrapsEoseManager.kt | 52 +- .../nip59GiftWraps/FilterGiftWrapsToPubkey.kt | 25 +- .../ChannelFinderFilterAssemblyGroup.kt | 2 +- .../reqCommand/channel/ChannelObservers.kt | 10 +- ...adataAndLiveActivityWatcherSubAssembler.kt | 48 +- .../ChannelLoaderSubAssembler.kt | 6 +- .../ChannelMetadataWatcherSubAssembler.kt | 16 +- .../FilterChannelMetadataCreationById.kt | 52 +- .../FilterChannelMetadataUpdatesById.kt | 26 +- .../FilterLiveStreamUpdatesByAddress.kt | 29 +- .../LiveActivityWatcherSubAssembly.kt | 19 +- .../event/EventFinderFilterAssembler.kt | 2 +- .../loaders/FilterMissingAddressables.kt | 69 +- .../event/loaders/FilterMissingEvents.kt | 34 +- .../loaders/NoteEventLoaderSubAssembler.kt | 2 +- .../watchers/EventWatcherSubAssembler.kt | 35 +- .../FilterRepliesAndReactionsToAddresses.kt | 118 +- .../FilterRepliesAndReactionsToNotes.kt | 122 +- .../nwc/FilterNWCPaymentsFromRequests.kt | 32 +- .../NWCFinderFilterAssemblerSubscription.kt | 68 + .../nwc/NWCPaymentFilterAssembler.kt | 11 +- .../nwc/NWCPaymentWatcherSubAssembler.kt | 31 +- .../user/UserFinderFilterAssembler.kt | 4 +- .../UserFinderFilterAssemblerSubscription.kt | 12 +- .../reqCommand/user/UserObservers.kt | 14 +- .../user/loaders/FilterUserMetadataForKey.kt | 59 +- .../user/loaders/UserLoaderSubAssembler.kt | 31 +- .../user/watchers/FilterReportsToKey.kt | 41 +- .../user/watchers/FilterUserMetadataForKey.kt | 55 +- .../user/watchers/UserWatcherSubAssembler.kt | 50 +- .../searchCommand/SearchFilterAssembler.kt | 6 +- .../subassemblies/FilterByAddress.kt | 44 +- .../subassemblies/FilterByAuthor.kt | 18 +- .../subassemblies/FilterByEvent.kt | 15 +- .../subassemblies/SearchPeopleByName.kt | 32 +- .../subassemblies/SearchPostsByText.kt | 128 +- .../SearchWatcherSubAssembler.kt | 49 +- .../amethyst/service/relays/EOSE.kt | 32 +- .../uploads/blossom/BlossomUploader.kt | 6 +- .../service/uploads/nip96/Nip96Uploader.kt | 7 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 16 +- .../amethyst/ui/actions/EditPostView.kt | 4 +- .../amethyst/ui/actions/EditPostViewModel.kt | 8 +- .../amethyst/ui/actions/NewMediaModel.kt | 8 +- .../amethyst/ui/actions/NewMediaView.kt | 4 +- .../ui/actions/RelaySelectionDialog.kt | 62 +- .../mediaServers/BlossomServersViewModel.kt | 2 +- .../mediaServers/NIP96ServersViewModel.kt | 2 +- .../amethyst/ui/components/RichTextViewer.kt | 13 +- .../markdown/RenderContentAsMarkdown.kt | 2 +- .../amethyst/ui/dal/FilterByListParams.kt | 76 +- .../amethyst/ui/navigation/AppNavigation.kt | 15 +- .../amethyst/ui/navigation/BottomBarRoute.kt | 34 + .../amethyst/ui/navigation/DrawerContent.kt | 10 +- .../NormalizedRelayUrlSerializer.kt | 37 +- .../amethyst/ui/navigation/RouteMaker.kt | 4 +- .../amethyst/ui/navigation/Routes.kt | 17 +- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 2 +- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/ReactionsRow.kt | 13 + .../amethyst/ui/note/RelayCompose.kt | 3 +- .../amethyst/ui/note/RelayListRow.kt | 17 +- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 13 +- .../amethyst/ui/note/UserProfilePicture.kt | 2 +- .../creators/uploads/ImageVideoDescription.kt | 2 +- .../userSuggestions/UserSuggestionState.kt | 2 +- .../ui/note/elements/DisplayHashtags.kt | 5 +- .../ui/note/elements/DisplayLocation.kt | 2 + .../ui/note/elements/DisplayReward.kt | 2 - .../note/elements/DisplayUncitedHashtags.kt | 5 +- .../nip22Comments/CommentPostViewModel.kt | 21 +- .../note/nip22Comments/DisplayExternalId.kt | 6 +- .../nip22Comments/DisplayGeoHashExternalId.kt | 17 +- .../nip22Comments/DisplayHashtagExternalId.kt | 17 +- .../nip22Comments/GenericCommentPostScreen.kt | 8 +- .../amethyst/ui/note/types/AudioTrack.kt | 2 +- .../amethyst/ui/note/types/CommunityHeader.kt | 5 +- .../amethyst/ui/note/types/Emoji.kt | 4 +- .../amethyst/ui/note/types/Git.kt | 3 +- .../types/NIP90ContentDiscoveryResponse.kt | 2 +- .../amethyst/ui/note/types/Poll.kt | 2 +- .../amethyst/ui/note/types/PrivateMessage.kt | 2 +- .../amethyst/ui/note/types/RelayList.kt | 25 +- .../amethyst/ui/note/types/Text.kt | 2 +- .../amethyst/ui/note/types/Video.kt | 2 +- .../ui/screen/AccountStateViewModel.kt | 25 +- .../amethyst/ui/screen/FollowListState.kt | 14 +- .../ui/screen/loggedIn/AccountViewModel.kt | 142 +- .../ui/screen/loggedIn/LoggedInPage.kt | 98 +- .../loggedIn/chats/feed/ChatMessageCompose.kt | 2 +- .../feed/types/RenderCreateChannelNote.kt | 38 +- .../datasource/ChatroomFilterAssembler.kt | 2 +- .../datasource/ChatroomFilterSubAssembler.kt | 14 +- .../privateDM/datasource/FilterNip04DMs.kt | 86 +- .../privateDM/send/ChatNewMessageViewModel.kt | 2 +- .../datasource/ChannelFilterAssembler.kt | 2 +- .../ChannelFromUserFilterSubAssembler.kt | 10 +- .../ChannelPublicFilterSubAssembler.kt | 10 +- .../FilterMessagesToEphemeralChat.kt | 26 +- .../FilterMessagesToLiveStream.kt | 25 +- .../FilterMessagesToPublicChat.kt | 25 +- .../FilterMyMessagesToEphemeralChat.kt | 26 +- .../FilterMyMessagesToLiveActivities.kt | 25 +- .../FilterMyMessagesToPublicChat.kt | 25 +- .../header/ShortEphemeralChatChannelHeader.kt | 24 +- .../metadata/NewEphemeralChatMetaViewModel.kt | 5 +- .../metadata/NewEphemeralChatScreen.kt | 6 +- .../metadata/ChannelMetadataScreen.kt | 2 +- .../metadata/ChannelMetadataViewModel.kt | 26 +- .../LongLiveActivityChannelHeader.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 2 +- .../chats/rooms/ChatroomHeaderCompose.kt | 8 +- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 6 +- .../datasource/ChatroomListFilterAssembler.kt | 2 +- .../DMsFromUserFilterSubAssembler.kt | 58 +- .../FilterFollowingEphemeralChats.kt | 49 +- .../datasource/FilterFollowingPublicChats.kt | 49 +- .../FilterLastMessageFollowingPublicChats.kt | 84 +- .../rooms/datasource/FilterNip04DMsFromMe.kt | 43 + .../datasource/FilterNip04DMsToAndFromMe.kt | 56 - .../rooms/datasource/FilterNip04DMsToMe.kt | 43 + .../FollowingEphemeralChatSubAssembler.kt | 12 +- .../FollowingPublicChatSubAssembler.kt | 18 +- .../chats/utils/ChatFileUploadDialog.kt | 2 +- .../CommunityFeedFilterSubAssembler.kt | 25 +- .../datasource/CommunityFilterAssembler.kt | 2 +- .../datasource/FilterCommunityPosts.kt | 21 +- .../datasource/DiscoveryFilterAssembler.kt | 7 +- ...DiscoveryFollowsDiscoverySubAssembler1.kt} | 57 +- .../DiscoveryFollowsDiscoverySubAssembler2.kt | 94 + ...DiscoveryFollowsDiscoverySubAssembler3.kt} | 46 +- ...MixGeohashHashtagsDiscoverySubAssembler.kt | 123 - .../DiscoverLongFormFeedFilter.kt | 6 +- .../nip23LongForm/SubAssemblyHelper.kt | 57 + .../FilterLongFormByAllCommunities.kt | 85 + .../subassemblies/FilterLongFormByAuthors.kt | 95 + .../FilterLongFormByCommunity.kt | 88 + .../subassemblies/FilterLongFormByFollows.kt | 52 +- .../subassemblies/FilterLongFormByGeohash.kt | 50 +- .../subassemblies/FilterLongFormByHashtag.kt | 54 +- .../subassemblies/FilterLongFormGlobal.kt | 50 + .../nip28Chats/DiscoverChatFeedFilter.kt | 21 +- .../discover/nip28Chats/RenderChannelThumb.kt | 20 +- .../discover/nip28Chats/SubAssemblyHelper.kt | 57 + .../FilterPublicChatsByAllCommunities.kt | 92 + .../FilterPublicChatsByAuthors.kt | 102 + .../FilterPublicChatsByCommunity.kt | 95 + .../FilterPublicChatsByFollows.kt | 52 +- .../FilterPublicChatsByGeohash.kt | 49 +- .../FilterPublicChatsByHashtag.kt | 49 +- .../subassemblies/FilterPublicChatsGlobal.kt} | 48 +- .../DiscoverFollowSetsFeedFilter.kt | 6 +- .../nip51FollowSets/SubAssemblyHelper.kt | 57 + .../FilterFollowSetsByAllCommunities.kt | 86 + .../FilterFollowSetsByAuthors.kt | 95 + .../FilterFollowSetsByCommunity.kt | 88 + .../FilterFollowSetsByFollows.kt | 52 +- .../FilterFollowSetsByGeohash.kt | 53 +- .../FilterFollowSetsByHashtag.kt | 56 +- .../subassemblies/FilterFollowSetsGlobal.kt} | 45 +- .../DiscoverLiveFeedFilter.kt | 26 +- .../nip53LiveActivities/LiveActivityCard.kt | 17 +- .../nip53LiveActivities/SubAssemblyHelper.kt | 57 + .../FilterLiveActivitiesByAllCommunities.kt | 87 + .../FilterLiveActivitiesByAuthors.kt | 107 + .../FilterLiveActivitiesByCommunity.kt | 89 + .../FilterLiveActivitiesByFollows.kt | 71 +- .../FilterLiveActivitiesByGeohash.kt | 55 +- .../FilterLiveActivitiesByHashtag.kt | 55 +- .../FilterLiveActivitiesGlobal.kt} | 49 +- .../nip72Communities/CommunityCard.kt | 20 +- .../DiscoverCommunityFeedFilter.kt | 20 +- .../nip72Communities/SubAssemblyHelper.kt | 57 + .../FilterCommunitiesByAllCommunities.kt | 81 + .../FilterCommunitiesByAuthors.kt | 96 + .../FilterCommunitiesByCommunity.kt | 71 + .../FilterCommunitiesByFollows.kt | 57 +- .../FilterCommunitiesByGeohash.kt | 75 + .../FilterCommunitiesByHashtag.kt | 76 + ...yGeohash.kt => FilterCommunitiesGlobal.kt} | 51 +- .../nip90DVMs/DiscoverNIP89FeedFilter.kt | 6 +- .../discover/nip90DVMs/SubAssemblyHelper.kt | 57 + .../FilterContentDVMsByAllCommunities.kt | 85 + .../FilterContentDVMsByAuthors.kt | 96 + .../FilterContentDVMsByCommunity.kt | 88 + .../FilterContentDVMsByFollows.kt | 52 +- .../FilterContentDVMsByGeohash.kt | 49 +- .../FilterContentDVMsByHashtag.kt | 49 +- .../subassemblies/FilterContentDVMsGlobal.kt | 53 + .../DiscoverMarketplaceFeedFilter.kt | 6 +- .../nip99Classifieds/NewProductScreen.kt | 7 +- .../nip99Classifieds/NewProductViewModel.kt | 15 +- .../nip99Classifieds/SubAssemblyHelper.kt | 57 + .../FilterClassifiedsByAllCommunities.kt | 85 + .../FilterClassifiedsByAuthors.kt | 95 + .../FilterClassifiedsByCommunity.kt | 88 + .../FilterClassifiedsByFollows.kt | 52 +- .../FilterClassifiedsByGeohash.kt | 49 +- .../FilterClassifiedsByHashtag.kt | 49 +- .../subassemblies/FilterClassifiedsGlobal.kt | 52 + .../dvms/DvmContentDiscoveryScreen.kt | 2 +- .../NIP90ContentDiscoveryResponseFilter.kt | 4 +- .../screen/loggedIn/geohash/GeoHashScreen.kt | 28 +- .../loggedIn/geohash/dal/GeoHashFeedFilter.kt | 7 +- .../geohash/dal/GeoHashFeedViewModel.kt | 7 +- .../datasource/FilterPostsByGeohash.kt | 94 +- .../GeoHashFeedFilterSubAssembler.kt | 10 +- .../datasource/GeoHashFilterAssembler.kt | 4 +- .../GeoHashFilterAssemblerSubscription.kt | 12 +- .../screen/loggedIn/hashtag/HashtagScreen.kt | 24 +- .../loggedIn/hashtag/dal/HashtagFeedFilter.kt | 4 +- .../hashtag/dal/HashtagFeedViewModel.kt | 7 +- .../datasource/FilterPostsByHashtags.kt | 120 +- .../HashtagFeedFilterSubAssembler.kt | 10 +- .../datasource/HashtagFilterAssembler.kt | 4 +- .../HashtagFilterAssemblerSubscription.kt | 11 +- .../loggedIn/home/ShortNotePostScreen.kt | 7 +- .../loggedIn/home/ShortNotePostViewModel.kt | 21 +- .../home/dal/HomeConversationsFeedFilter.kt | 11 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 54 +- .../home/dal/HomeNewThreadFeedFilter.kt | 24 +- .../home/datasource/HomeFilterAssembler.kt | 8 +- .../MixGeohashHashtagsCommunityEoseManager.kt | 83 - .../nip01Core/FilterHomePostsByGeohashes.kt | 101 + .../FilterHomePostsByGlobal.kt} | 112 +- .../nip01Core/FilterHomePostsByHashtags.kt | 107 + .../nip01Geohash/FilterPostsByGeohashes.kt | 66 - .../nip01Geohash/GeohashEventsEoseManager.kt | 78 - .../FilterHomePostsByHashtags.kt | 71 - .../HashtagEventsFilterSubAssembler.kt | 80 - ...eohashScopes.kt => FilterPostsByScopes.kt} | 26 +- .../FilterHomePostsByAllFollows.kt | 66 + .../nip65Follows/FilterHomePostsByAuthors.kt | 142 + .../HomeOutboxEventsEoseManager.kt | 44 +- .../CommunityEventsFilterSubAssembler.kt | 76 - .../FilterHomePostsByAllCommunities.kt | 84 + .../FilterHomePostsFromCommunities.kt | 102 +- .../dal/NotificationFeedFilter.kt | 13 +- .../datasource/FilterUserProfileFollowers.kt | 37 +- .../datasource/FilterUserProfileLists.kt | 55 +- .../datasource/FilterUserProfileMedia.kt | 55 +- .../datasource/FilterUserProfileMetadata.kt | 55 +- .../datasource/FilterUserProfilePosts.kt | 99 +- .../FilterUserProfileZapReceived.kt | 37 +- .../datasource/UserProfileFilterAssembler.kt | 2 +- .../UserProfileFollowersFilterSubAssembler.kt | 13 +- .../UserProfileMediaFilterSubAssembler.kt | 14 +- .../UserProfileMetadataFilterSubAssembler.kt | 26 +- .../UserProfilePostsFilterSubAssembler.kt | 14 +- .../UserProfileZapsFilterSubAssembler.kt | 12 +- .../dal/UserProfileGalleryFeedFilter.kt | 4 +- .../profile/hashtags/TabFollowedTags.kt | 6 +- .../loggedIn/profile/relays/RelayFeedView.kt | 4 +- .../profile/relays/RelayFeedViewModel.kt | 10 +- .../screen/loggedIn/qrcode/QrCodeScanner.kt | 8 +- .../ui/screen/loggedIn/qrcode/ShowQRDialog.kt | 2 +- .../loggedIn/relays/AllRelayListScreen.kt | 46 - .../loggedIn/relays/RelayInformationDialog.kt | 23 +- .../relays/common/BasicRelaySetupInfo.kt | 16 +- .../common/BasicRelaySetupInfoClickableRow.kt | 9 +- .../common/BasicRelaySetupInfoDialog.kt | 17 +- .../relays/common/BasicRelaySetupInfoModel.kt | 17 +- .../relays/common/RelayNameAndRemoveButton.kt | 5 +- .../relays/common/RelayUrlEditField.kt | 18 +- .../loggedIn/relays/dm/DMRelayListView.kt | 2 +- .../relays/dm/DMRelayListViewModel.kt | 5 +- .../relays/kind3/Kind3RelayListView.kt | 824 ----- .../relays/kind3/Kind3RelayListViewModel.kt | 293 -- .../relays/local/LocalRelayListView.kt | 2 +- .../relays/local/LocalRelayListViewModel.kt | 7 +- .../nip37/PrivateOutboxRelayListView.kt | 2 +- .../nip37/PrivateOutboxRelayListViewModel.kt | 5 +- .../relays/nip65/Nip65RelayListView.kt | 4 +- .../relays/nip65/Nip65RelayListViewModel.kt | 36 +- .../Kind3RelaySetupInfoProposalDialog.kt | 118 - .../Kind3RelaySetupInfoProposalRow.kt | 199 -- .../relays/search/SearchRelayListView.kt | 2 +- .../relays/search/SearchRelayListViewModel.kt | 5 +- .../loggedIn/search/SearchBarViewModel.kt | 4 +- .../settings/SecurityFiltersScreen.kt | 4 +- .../settings/dal/HiddenAccountsFeedFilter.kt | 2 +- .../settings/dal/HiddenWordsFeedFilter.kt | 2 +- .../settings/dal/SpammerAccountsFeedFilter.kt | 2 +- .../loggedIn/threadview/ThreadFeedView.kt | 2 +- .../threadview/dal/ThreadFeedFilter.kt | 2 +- .../datasources/ThreadFilterAssembler.kt | 2 +- .../FilterEventsInThreadForRoot.kt | 109 +- .../FilterMissingEventsForThread.kt | 7 +- .../ThreadEventLoaderSubAssembler.kt | 10 +- .../subassembies/ThreadFilterSubAssembler.kt | 10 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 2 +- .../loggedIn/video/dal/VideoFeedFilter.kt | 4 +- ...ctureAndVideoByGeohash.kt => FeedBasis.kt} | 48 +- .../video/datasource/VideoFilterAssembler.kt | 7 +- .../FilterPictureAndVideoByFollows.kt | 64 - .../FilterPictureAndVideoByHashtag.kt | 72 - ...deoMixGeohashHashtagsFilterSubAssembler.kt | 79 - .../VideoOutboxEventsFilterSubAssembler.kt | 51 +- .../FilterPictureAndVideoByGeohash.kt | 86 + .../FilterPictureAndVideoByHashtag.kt | 89 + .../nip01Core/FilterPictureAndVideoGlobal.kt | 61 + .../FilterPictureAndVideoByAuthors.kt | 108 + .../FilterPictureAndVideoByFollows.kt | 56 + .../FilterPictureAndVideoByAllCommunities.kt | 117 + .../FilterPictureAndVideoByCommunity.kt | 124 + .../ammolite/relays/Constants.kt | 57 - .../ammolite/relays/NostrClient.kt | 475 --- .../vitorpamplona/ammolite/relays/Relay.kt | 274 -- .../ammolite/relays/RelayPool.kt | 311 -- .../relays/datasources/NostrDataSource.kt | 276 -- .../relays/datasources/Subscription.kt | 106 +- .../datasources/SubscriptionController.kt | 36 +- .../relays/datasources/SubscriptionSet.kt | 6 +- .../filters/{EOSETime.kt => MutableTime.kt} | 19 +- .../ammolite/relays/filters/NormalFilter.kt | 57 - .../filters/SinceAuthorPerRelayFilter.kt | 89 - .../relays/filters/SincePerRelayFilter.kt | 69 - .../quartz/benchmark/HintIndexerBenchmark.kt | 2 +- .../commons/base64Image/Base64Image.kt | 2 +- gradle/libs.versions.toml | 3 +- quartz/build.gradle | 1 - quartz/notebooks/Kind1Test.ipynb | 147 + .../quartz/nip03Timestamp/ots/OtsTest.kt | 4 +- .../com/vitorpamplona/quartz/EventFactory.kt | 10 +- .../audio/track/tags/ParticipantTag.kt | 10 +- .../edits/PrivateOutboxRelayListEvent.kt | 35 +- .../experimental/edits/tags/RelayTag.kt | 52 + .../ephemChat/chat/EphemeralChatEvent.kt | 9 +- .../experimental/ephemChat/chat/RoomId.kt | 7 +- .../ephemChat/chat/TagArrayBuilderExt.kt | 3 +- .../ephemChat/chat/tags/RelayTag.kt | 13 +- .../ephemChat/chat/tags/RoomTag.kt | 6 +- .../ephemChat/db/EphemeralRoom.kt | 3 +- .../ephemChat/db/EphemeralRoomCache.kt | 5 +- .../ephemChat/list/EphemeralChatListEvent.kt | 10 +- .../ephemChat/list/tags/RoomIdTag.kt | 16 +- .../experimental/forks/MarkedETagExt.kt | 5 +- .../InteractiveStoryReadingStateEvent.kt | 7 +- .../interactiveStories/tags/RootSceneTag.kt | 22 +- .../experimental/medical/FhirResourceEvent.kt | 3 +- .../profileGallery/TagArrayBuilderExt.kt | 3 +- .../quartz/nip01Core/core/AddressableEvent.kt | 3 +- .../nip01Core/core/BaseAddressableEvent.kt | 3 +- .../nip01Core/core/BaseReplaceableEvent.kt | 3 +- .../quartz/nip01Core/hints/EventHintBundle.kt | 12 +- .../quartz/nip01Core/hints/HintIndexer.kt | 36 +- .../nip01Core/hints/types/AddressHint.kt | 4 +- .../nip01Core/hints/types/EventIdHint.kt | 10 +- .../nip01Core/hints/types/PubKeyHint.kt | 3 +- .../nip01Core/relay/client/NostrClient.kt | 276 ++ .../client/acessories}/EventCollector.kt | 15 +- .../NostrClientSingleDownloadExt.kt | 97 + .../client/acessories}/RelayAuthenticator.kt | 13 +- .../RelayInsertConfirmationCollector.kt | 13 +- .../relay/client/acessories}/RelayLogger.kt | 19 +- .../relay/client/acessories}/RelayNotifier.kt | 13 +- .../client/listeners/IRelayClientListener.kt | 138 + .../listeners/RedirectRelayClientListener.kt | 94 + .../relay/client/pool/PoolEventOutbox.kt | 91 + .../client/pool/PoolEventOutboxRepository.kt | 92 + .../relay/client/pool/PoolSubscription.kt | 14 +- .../client/pool/PoolSubscriptionRepository.kt | 87 + .../relay/client/pool/RelayBasedFilter.kt | 43 + .../nip01Core/relay/client/pool/RelayPool.kt | 299 ++ .../relay/client/single/IRelayClient.kt | 60 + .../relay/{ => client/single}/Subscription.kt | 8 +- .../single/basic/BasicRelayClient.kt} | 360 +- .../client/single/simple/OutboxProtector.kt | 88 + .../client/single/simple/SimpleRelayClient.kt | 46 + .../relay/{ => client/stats}/RelayStat.kt | 2 +- .../relay/client/stats}/RelayStats.kt | 24 +- .../relay/normalizer/NormalizedRelayUrl.kt | 44 + .../relay/normalizer/RelayUrlNormalizer.kt | 90 + .../nip01Core/relay/sockets/WebSocket.kt | 4 +- .../relay/sockets/WebSocketListener.kt | 1 + .../relay/sockets/WebsocketBuilder.kt | 4 +- .../quartz/nip01Core/signers/NostrSigner.kt | 2 + .../nip01Core/signers/NostrSignerInternal.kt | 2 + .../nip01Core/tags/addressables/ATag.kt | 42 +- .../nip01Core/tags/addressables/Address.kt | 32 +- .../nip01Core/tags/addressables/EventExt.kt | 4 +- .../tags/addressables/TagArrayBuilderExt.kt | 1 + .../quartz/nip01Core/tags/events/ETag.kt | 28 +- .../nip01Core/tags/events/EventReference.kt | 3 +- .../nip01Core/tags/events/GenericETag.kt | 3 +- .../tags/events/TagArrayBuilderExt.kt | 1 + .../nip01Core/tags/hashtags/HashtagTag.kt | 10 + .../quartz/nip01Core/tags/people/PTag.kt | 21 +- .../tags/people/PubKeyReferenceTag.kt | 3 +- .../tags/people/TagArrayBuilderExt.kt | 3 +- .../nip02FollowList/ContactListEvent.kt | 19 +- .../quartz/nip02FollowList/tags/ContactTag.kt | 28 +- .../quartz/nip10Notes/BaseThreadedEvent.kt | 10 +- .../quartz/nip10Notes/tags/MarkedETag.kt | 103 +- .../quartz/nip10Notes/tags/ReplyBuilder.kt | 4 +- .../nip17Dm/messages/ChatMessageEvent.kt | 3 +- .../settings/ChatMessageRelayListEvent.kt | 32 +- .../quartz/nip17Dm/settings/tags/RelayTag.kt | 52 + .../quartz/nip18Reposts/GenericRepostEvent.kt | 5 +- .../quartz/nip18Reposts/RepostEvent.kt | 5 +- .../quartz/nip18Reposts/quotes/EntityExt.kt | 7 + .../nip18Reposts/quotes/QAddressableTag.kt | 21 +- .../quartz/nip18Reposts/quotes/QEventTag.kt | 17 +- .../quartz/nip18Reposts/quotes/QTag.kt | 24 +- .../quartz/nip19Bech32/ATagExt.kt | 9 +- .../quartz/nip19Bech32/EventExt.kt | 3 +- .../quartz/nip19Bech32/TlvBuilderExt.kt | 5 + .../quartz/nip19Bech32/entities/NAddress.kt | 31 +- .../quartz/nip19Bech32/entities/NEvent.kt | 32 +- .../quartz/nip19Bech32/entities/NProfile.kt | 10 +- .../quartz/nip19Bech32/entities/NSec.kt | 4 + .../quartz/nip19Bech32/tlv/TlvBuilder.kt | 9 + .../quartz/nip21UriScheme/EventExt.kt | 3 +- .../nip22Comments/TagArrayBuilderExt.kt | 13 +- .../nip22Comments/tags/ReplyAddressTag.kt | 21 +- .../nip22Comments/tags/ReplyAuthorTag.kt | 19 +- .../nip22Comments/tags/ReplyEventTag.kt | 16 +- .../nip22Comments/tags/RootAddressTag.kt | 21 +- .../nip22Comments/tags/RootAuthorTag.kt | 24 +- .../quartz/nip22Comments/tags/RootEventTag.kt | 19 +- .../nip23LongContent/LongTextNoteEvent.kt | 3 +- .../admin/ChannelCreateEvent.kt | 22 +- .../admin/ChannelMetadataEvent.kt | 27 +- .../nip28PublicChat/base/ChannelData.kt | 24 +- .../nip28PublicChat/list/ChannelListEvent.kt | 10 +- .../list/TagArrayBuilderExt.kt | 3 +- .../nip34Git/repository/GitRepositoryEvent.kt | 3 +- .../ContentWarningTag.kt | 18 +- .../TagArrayBuilderExt.kt | 2 + .../nip36SensitiveContent/TagArrayExt.kt | 11 +- .../quartz/nip37Drafts/DraftEvent.kt | 20 +- .../quartz/nip40Expiration/EventExt.kt | 12 +- .../quartz/nip40Expiration/ExpirationTag.kt | 45 + .../nip40Expiration/TagArrayBuilderExt.kt | 26 + .../quartz/nip40Expiration/TagArrayExt.kt | 36 + .../quartz/nip42RelayAuth/RelayAuthEvent.kt | 23 +- .../nip42RelayAuth/tags/ChallengeTag.kt | 41 + .../quartz/nip42RelayAuth/tags/RelayTag.kt | 52 + .../nip47WalletConnect/Nip47WalletConnect.kt | 22 +- .../nip50Search/SearchRelayListEvent.kt | 27 +- .../quartz/nip50Search/tags/RelayTag.kt | 52 + .../quartz/nip51Lists/MuteListEvent.kt | 22 +- .../quartz/nip51Lists/PeopleListEvent.kt | 16 + .../quartz/nip51Lists/RelaySetEvent.kt | 4 +- .../nip51Lists/interests/HashtagListEvent.kt | 218 ++ .../interests/TagArrayBuilderExt.kt | 26 + .../nip51Lists/locations/GeohashListEvent.kt | 219 ++ .../locations/TagArrayBuilderExt.kt | 26 + .../quartz/nip51Lists/tags/RelayTag.kt | 52 + .../streaming/LiveActivitiesEvent.kt | 25 +- .../streaming/tags/ParticipantTag.kt | 28 +- .../streaming/tags/RelayListTag.kt | 20 +- .../quartz/nip54Wiki/WikiNoteEvent.kt | 3 +- .../nip55AndroidSigner/NostrSignerExternal.kt | 2 + .../quartz/nip56Reports/ReportEvent.kt | 20 +- .../quartz/nip57Zaps/LnZapEvent.kt | 21 +- .../quartz/nip57Zaps/LnZapPrivateEvent.kt | 20 +- .../quartz/nip57Zaps/LnZapRequestEvent.kt | 20 +- .../quartz/nip57Zaps/splits/ZapSplitSetup.kt | 3 +- .../nip57Zaps/splits/ZapSplitSetupParser.kt | 5 +- .../splits/ZapSplitSetupSerializer.kt | 4 +- .../quartz/nip58Badges/BadgeAwardEvent.kt | 20 +- .../quartz/nip58Badges/BadgeProfilesEvent.kt | 18 +- .../AdvertisedRelayListEvent.kt | 97 +- .../RelayListRecommendationProcessor.kt | 56 +- .../nip65RelayList/RelayUrlFormatter.kt | 83 - .../tags/AdvertisedRelayInfoTag.kt | 125 + .../nip72ModCommunities/CommunityListEvent.kt | 279 -- .../approval/CommunityPostApprovalEvent.kt | 24 +- .../definition/CommunityDefinitionEvent.kt | 28 +- .../definition/tags/ModeratorTag.kt | 28 +- .../definition/tags/RelayTag.kt | 24 +- .../follow/CommunityListEvent.kt | 272 ++ .../follow/TagArrayBuilderExt.kt | 31 + .../quartz/nip75ZapGoals/GoalEvent.kt | 20 +- .../quartz/nip84Highlights/HighlightEvent.kt | 23 +- .../nip89AppHandlers/clientTag/ClientTag.kt | 77 + .../nip89AppHandlers/clientTag/EventExt.kt | 26 + .../clientTag/TagArrayBuilderExt.kt} | 21 +- .../nip89AppHandlers/clientTag/TagArrayExt.kt | 25 + .../recommendation/AppRecommendationEvent.kt | 9 +- .../recommendation/TagArrayBuilderExt.kt | 5 +- .../recommendation/tags/RecommendationTag.kt | 30 +- .../NIP90ContentDiscoveryRequestEvent.kt | 5 +- .../BlossomAuthorizationEvent.kt | 2 +- .../BlossomServersEvent.kt | 2 +- .../vitorpamplona/quartz/utils/LargeCache.kt | 17 + .../quartz/utils/MapOfSetBuilder.kt | 73 + .../quartz/utils}/ParallelUtils.kt | 2 +- .../quartz/utils/RandomInstance.kt | 7 + quartz/src/test/java/android/util/Log.java | 35 + .../quartz/nip01Core/hints/HintIndexerTest.kt | 15 +- .../nip01Core/relay/RelayUrlFormatterTest.kt | 47 + .../quartz/nip19Bech32/NIP19ParserTest.kt | 39 +- .../RelayListRecommendationProcessorTest.kt | 31 +- 624 files changed, 19405 insertions(+), 11339 deletions(-) create mode 100644 .idea/deviceManager.xml create mode 100644 .idea/kotlinNotebook.xml delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/{service/relays/RelayManager.kt => model/nip01UserMetadata/NotificationInboxRelayState.kt} (61%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/{service/relays/RelayServiceStatus.kt => model/topNavFeeds/IFeedFlowsType.kt} (78%) rename ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/IPerRelayFilter.kt => amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt (72%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/{ui/screen/loggedIn/relays/kind3/Kind3BasicRelaySetupInfo.kt => model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilter.kt} (60%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilterSet.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt rename ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayBriefInfoCache.kt => amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt (61%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt rename ammolite/src/main/java/com/vitorpamplona/ammolite/relays/TypedFilter.kt => amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt (77%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt rename ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelaySetupInfo.kt => amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt (74%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilterSet.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilterSet.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/{ui/screen/loggedIn/relays/recommendations/Kind3RelayProposalSetupInfo.kt => model/topNavFeeds/unknown/UnknownTopNavFilter.kt} (62%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayState.kt => amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt (83%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/{relays/RelayService.kt => okhttp/ProxySettingsAnchor.kt} (57%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/BottomBarRoute.kt rename ammolite/src/main/java/com/vitorpamplona/ammolite/relays/MutableSubscriptionCache.kt => amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt (57%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToAndFromMe.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/{DiscoveryFollowsDiscoverySubAssembler.kt => DiscoveryFollowsDiscoverySubAssembler1.kt} (62%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/{home/datasource/nip65Follows/HomeOutboxUsersEoseManager.kt => discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt} (60%) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/MixGeohashHashtagsDiscoverySubAssembler.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/{nip72Communities/subassemblies/FilterCommunityPostsByHashtag.kt => nip28Chats/subassemblies/FilterPublicChatsGlobal.kt} (54%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/{service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt => ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt} (54%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/{home/datasource/nip65Follows/FilterUserMetadataByFollows.kt => discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt} (50%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/{FilterCommunityPostsByGeohash.kt => FilterCommunitiesGlobal.kt} (57%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/mixGeohashHashtagsCommunities/MixGeohashHashtagsCommunityEoseManager.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/{nip65Follows/FilterHomePostsByFollows.kt => nip01Core/FilterHomePostsByGlobal.kt} (51%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/FilterPostsByGeohashes.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/GeohashEventsEoseManager.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/FilterHomePostsByHashtags.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/HashtagEventsFilterSubAssembler.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/{FilterPostsByGeohashScopes.kt => FilterPostsByScopes.kt} (73%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/CommunityEventsFilterSubAssembler.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListView.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelaySetupInfoProposalDialog.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelaySetupInfoProposalRow.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/{subassemblies/FilterPictureAndVideoByGeohash.kt => FeedBasis.kt} (50%) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByFollows.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByHashtag.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoMixGeohashHashtagsFilterSubAssembler.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Constants.kt delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayPool.kt delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/NostrDataSource.kt rename ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/{EOSETime.kt => MutableTime.kt} (80%) delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/NormalFilter.kt delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SinceAuthorPerRelayFilter.kt delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SincePerRelayFilter.kt create mode 100644 quartz/notebooks/Kind1Test.ipynb create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/tags/RelayTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources => quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories}/EventCollector.kt (80%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources => quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories}/RelayAuthenticator.kt (79%) rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources => quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories}/RelayInsertConfirmationCollector.kt (81%) rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources => quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories}/RelayLogger.kt (79%) rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources => quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories}/RelayNotifier.kt (80%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/IRelayClientListener.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RedirectRelayClientListener.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxRepository.kt rename ammolite/src/main/java/com/vitorpamplona/ammolite/relays/SubscriptionCache.kt => quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscription.kt (77%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscriptionRepository.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/IRelayClient.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/{ => client/single}/Subscription.kt (86%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/{SimpleClientRelay.kt => client/single/basic/BasicRelayClient.kt} (54%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/{ => client/stats}/RelayStat.kt (97%) rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays => quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/stats}/RelayStats.kt (74%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/tags/RelayTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/ExpirationTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/tags/ChallengeTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/tags/RelayTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/tags/RelayTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/RelayTag.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayUrlFormatter.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/ClientTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/EventExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip01Core/relay/SubscriptionCollection.kt => nip89AppHandlers/clientTag/TagArrayBuilderExt.kt} (73%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/TagArrayExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{blossom => nipB7Blossom}/BlossomAuthorizationEvent.kt (98%) rename quartz/src/main/java/com/vitorpamplona/quartz/{blossom => nipB7Blossom}/BlossomServersEvent.kt (98%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt rename {amethyst/src/main/java/com/vitorpamplona/amethyst => quartz/src/main/java/com/vitorpamplona/quartz/utils}/ParallelUtils.kt (98%) create mode 100644 quartz/src/test/java/android/util/Log.java create mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/relay/RelayUrlFormatterTest.kt diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000000..91f95584dd --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/kotlinNotebook.xml b/.idea/kotlinNotebook.xml new file mode 100644 index 0000000000..665e38dae3 --- /dev/null +++ b/.idea/kotlinNotebook.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index bb4493707f..bdfaa5e068 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,5 +1,11 @@ + + + + diff --git a/README.md b/README.md index 97d6e99dfb..bab0a73aaf 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ Lastly, the user's account information (private key/pub key) is stored in the An ## Setup Make sure to have the following pre-requisites installed: -1. Java 17+ +1. Java 21+ 2. Android Studio 3. Android 8.0+ Phone or Emulation setup diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 91b9e0ad57..fc3031dba1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -37,17 +37,19 @@ import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket +import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector +import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector import com.vitorpamplona.amethyst.service.relayClient.RelaySpeedLogger import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory import com.vitorpamplona.amethyst.ui.tor.TorManager -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip03Timestamp.VerificationStateCache import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -89,18 +91,26 @@ class Amethyst : Application() { scope = applicationIOScope, ) - // Connects the NostrClient class with okHttp - val factory = - OkHttpWebSocket.BuilderFactory { _, useProxy -> - okHttpClients.getHttpClient(useProxy) - } + val torProxySettingsAnchor = ProxySettingsAnchor() - // Provides a relay pool - val client: NostrClient = NostrClient(factory) + // Connects the NostrClient class with okHttp + val websocketBuilder = + OkHttpWebSocket.Builder { url -> + okHttpClients.getHttpClient(torProxySettingsAnchor.useProxy(url)) + } // Caches all events in Memory val cache: LocalCache = LocalCache + // Organizes cache clearing + val trimmingService = MemoryTrimmingService(cache) + + // Provides a relay pool + val client: NostrClient = NostrClient(websocketBuilder, applicationIOScope) + + // Watches for changes on Tor and Relay List Settings + val relayProxyClientConnector = RelayProxyClientConnector(torProxySettingsAnchor, okHttpClients, connManager, client, applicationIOScope) + // Verifies and inserts in the cache from all relays, all subscriptions val cacheClientConnector = CacheClientConnector(client, cache) @@ -112,15 +122,9 @@ class Amethyst : Application() { val logger = if (isDebug) RelaySpeedLogger(client) else null - // Organizes cache clearing - val trimmingService = MemoryTrimmingService(cache) - // Coordinates all subscriptions for the Nostr Client val sources: RelaySubscriptionsCoordinator = RelaySubscriptionsCoordinator(LocalCache, client, applicationIOScope) - // Trash. - val serviceManager = ServiceManager(client, applicationIOScope) - // saves the .content of NIP-95 blobs in disk to save memory val nip95cache: File by lazy { Nip95CacheFactory.new(this) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 766ae8b776..7caf3ef17b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -33,9 +33,9 @@ import kotlin.time.measureTimedValue val isDebug = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark" fun debugState(context: Context) { - Amethyst.instance.client - .allSubscriptions() - .forEach { Log.d("STATE DUMP", "${it.key} ${it.value.joinToString { it.filter.toDebugJson() }}") } + // Amethyst.instance.client + // .allSubscriptions() + // .forEach { Log.d("STATE DUMP", "${it.key} ${it.value.filters.joinToString { it.filter.toJson() }}") } val totalMemoryMb = Runtime.getRuntime().totalMemory() / (1024 * 1024) val freeMemoryMb = Runtime.getRuntime().freeMemory() / (1024 * 1024) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 5349ffe8ff..b9c909df51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -27,16 +27,15 @@ import android.util.Log import androidx.compose.runtime.Immutable import androidx.core.content.edit import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.amethyst.model.ALL_FOLLOWS import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS -import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.Settings import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow -import com.vitorpamplona.ammolite.relays.RelaySetupInfo import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -53,7 +52,10 @@ import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -82,7 +84,6 @@ private object PrefKeys { const val SAVED_ACCOUNTS = "all_saved_accounts" const val NOSTR_PRIVKEY = "nostr_privkey" const val NOSTR_PUBKEY = "nostr_pubkey" - const val RELAYS = "relays" const val LOCAL_RELAY_SERVERS = "localRelayServers" const val DEFAULT_FILE_SERVER = "defaultFileServer" const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList" @@ -99,6 +100,9 @@ private object PrefKeys { const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList" const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData" const val LATEST_CHANNEL_LIST = "latestChannelList" + const val LATEST_COMMUNITY_LIST = "latestCommunityList" + const val LATEST_HASHTAG_LIST = "latestHashtagList" + const val LATEST_GEOHASH_LIST = "latestGeohashList" const val LATEST_EPHEMERAL_LIST = "latestEphemeralChatList" const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog" const val HIDE_BLOCK_ALERT_DIALOG = "hide_block_alert_dialog" @@ -120,8 +124,8 @@ object LocalPreferences { private const val COMMA = "," private var currentAccount: String? = null - private var savedAccounts: MutableStateFlow?> = MutableStateFlow(null) - private var cachedAccounts: MutableMap = mutableMapOf() + private val savedAccounts: MutableStateFlow?> = MutableStateFlow(null) + private val cachedAccounts: MutableMap = mutableMapOf() suspend fun currentAccount(): String? { if (currentAccount == null) { @@ -249,7 +253,7 @@ object LocalPreferences { if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub" Amethyst.instance.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE) } else { - return Amethyst.instance.encryptedStorage(npub) + Amethyst.instance.encryptedStorage(npub) } } @@ -299,7 +303,6 @@ object LocalPreferences { settings.keyPair.privKey?.let { putString(PrefKeys.NOSTR_PRIVKEY, it.toHexKey()) } } settings.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) } - putString(PrefKeys.RELAYS, EventMapper.mapper.writeValueAsString(settings.localRelays)) putString( PrefKeys.DEFAULT_FILE_SERVER, @@ -364,8 +367,8 @@ object LocalPreferences { remove(PrefKeys.LATEST_SEARCH_RELAY_LIST) } - if (settings.localRelayServers.isNotEmpty()) { - putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers) + if (settings.localRelayServers.value.isNotEmpty()) { + putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers.value) } else { remove(PrefKeys.LOCAL_RELAY_SERVERS) } @@ -406,6 +409,33 @@ object LocalPreferences { remove(PrefKeys.LATEST_CHANNEL_LIST) } + if (settings.backupCommunityList != null) { + putString( + PrefKeys.LATEST_COMMUNITY_LIST, + EventMapper.mapper.writeValueAsString(settings.backupCommunityList), + ) + } else { + remove(PrefKeys.LATEST_COMMUNITY_LIST) + } + + if (settings.backupHashtagList != null) { + putString( + PrefKeys.LATEST_HASHTAG_LIST, + EventMapper.mapper.writeValueAsString(settings.backupHashtagList), + ) + } else { + remove(PrefKeys.LATEST_HASHTAG_LIST) + } + + if (settings.backupGeohashList != null) { + putString( + PrefKeys.LATEST_HASHTAG_LIST, + EventMapper.mapper.writeValueAsString(settings.backupGeohashList), + ) + } else { + remove(PrefKeys.LATEST_HASHTAG_LIST) + } + if (settings.backupEphemeralChatList != null) { putString( PrefKeys.LATEST_EPHEMERAL_LIST, @@ -453,9 +483,8 @@ object LocalPreferences { prefs: SharedPreferences = encryptedPreferences(), ) { Log.d("LocalPreferences", "Saving to shared settings") - with(prefs.edit()) { + prefs.edit { putString(PrefKeys.SHARED_SETTINGS, EventMapper.mapper.writeValueAsString(sharedSettings)) - apply() } } @@ -514,7 +543,7 @@ object LocalPreferences { ?: if (getBoolean(PrefKeys.LOGIN_WITH_EXTERNAL_SIGNER, false)) "com.greenart7c3.nostrsigner" else null val defaultHomeFollowList = - getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS + getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: ALL_FOLLOWS val defaultStoriesFollowList = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS val defaultNotificationFollowList = @@ -522,8 +551,6 @@ object LocalPreferences { val defaultDiscoveryFollowList = getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS - val localRelays = parseOrNull>(PrefKeys.RELAYS) ?: emptySet() - val zapPaymentRequestServer = parseOrNull(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER) val defaultFileServer = parseOrNull(PrefKeys.DEFAULT_FILE_SERVER) ?: DEFAULT_MEDIA_SERVERS[0] @@ -538,8 +565,11 @@ object LocalPreferences { val latestMuteList = parseEventOrNull(PrefKeys.LATEST_MUTE_LIST) val latestPrivateHomeRelayList = parseEventOrNull(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST) val latestAppSpecificData = parseEventOrNull(PrefKeys.LATEST_APP_SPECIFIC_DATA) - val latestEphemeralList = parseEventOrNull(PrefKeys.LATEST_EPHEMERAL_LIST) val latestChannelList = parseEventOrNull(PrefKeys.LATEST_CHANNEL_LIST) + val latestCommunityList = parseEventOrNull(PrefKeys.LATEST_COMMUNITY_LIST) + val latestHashtagList = parseEventOrNull(PrefKeys.LATEST_HASHTAG_LIST) + val latestGeohashList = parseEventOrNull(PrefKeys.LATEST_GEOHASH_LIST) + val latestEphemeralList = parseEventOrNull(PrefKeys.LATEST_EPHEMERAL_LIST) val hideDeleteRequestDialog = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, false) val hideBlockAlertDialog = getBoolean(PrefKeys.HIDE_BLOCK_ALERT_DIALOG, false) @@ -559,14 +589,13 @@ object LocalPreferences { keyPair = keyPair, transientAccount = false, externalSignerPackageName = externalSignerPackageName, - localRelays = localRelays, - localRelayServers = localRelayServers, + localRelayServers = MutableStateFlow(localRelayServers), defaultFileServer = defaultFileServer, defaultHomeFollowList = MutableStateFlow(defaultHomeFollowList), defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList), defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList), defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList), - zapPaymentRequest = zapPaymentRequestServer, + zapPaymentRequest = zapPaymentRequestServer?.normalize(), hideDeleteRequestDialog = hideDeleteRequestDialog, hideBlockAlertDialog = hideBlockAlertDialog, hideNIP17WarningDialog = hideNIP17WarningDialog, @@ -579,6 +608,9 @@ object LocalPreferences { backupMuteList = latestMuteList, backupAppSpecificData = latestAppSpecificData, backupChannelList = latestChannelList, + backupCommunityList = latestCommunityList, + backupHashtagList = latestHashtagList, + backupGeohashList = latestGeohashList, backupEphemeralChatList = latestEphemeralList, torSettings = TorSettingsFlow.build(torSettings), lastReadPerRoute = MutableStateFlow(lastReadPerRoute), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt deleted file mode 100644 index bc464ebdc1..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ServiceManager.kt +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst - -import android.util.Log -import androidx.compose.runtime.Stable -import coil3.annotation.DelicateCoilApi -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.ammolite.relays.NostrClient -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -@Stable -class ServiceManager( - val client: NostrClient, - val scope: CoroutineScope, -) { - // to not open amber in a loop trying to use auth relays and registering for notifications - private var isStarted: Boolean = false - - private var account: Account? = null - - private var collectorJob: Job? = null - - private fun start(account: Account) { - this.account = account - start() - } - - @OptIn(DelicateCoilApi::class) - private fun start() { - Log.d("ServiceManager", "-- May Start (hasStarted: $isStarted) for account $account") - if (isStarted && account != null) { - Log.d("ServiceManager", "---- Restarting innactive relay Services with Tor: ${account?.settings?.torSettings?.torType?.value}") - client.reconnect() - return - } - Log.d("ServiceManager", "---- Starting Relay Services with Tor: ${account?.settings?.torSettings?.torType?.value}") - - val myAccount = account - - if (myAccount != null) { - val relaySet = myAccount.connectToRelaysWithProxy.value - client.reconnect(relaySet) - - collectorJob?.cancel() - collectorJob = null - collectorJob = - scope.launch { - myAccount.connectToRelaysWithProxy.collectLatest { - delay(500) - if (isStarted) { - client.reconnect(it, onlyIfChanged = true) - } - } - } - - isStarted = true - } - } - - private fun pause() { - Log.d("ServiceManager", "-- Pausing Relay Services") - - collectorJob?.cancel() - collectorJob = null - - client.reconnect(null) - isStarted = false - } - - fun cleanObservers() { - LocalCache.cleanObservers() - } - - // This method keeps the pause/start in a Syncronized block to - // avoid concurrent pauses and starts. - @Synchronized - fun forceRestart( - account: Account? = null, - start: Boolean = true, - pause: Boolean = true, - ) { - Log.d("ServiceManager", "-- Force Restart (start:$start) (pause:$pause) for $account") - if (pause) { - pause() - } - - if (start) { - if (account != null) { - start(account) - } else { - start() - } - } - } - - fun setAccountAndRestart(account: Account) { - forceRestart(account, true, true) - } - - fun forceRestart() { - forceRestart(null, true, true) - } - - fun justStartIfItHasAccount() { - if (account != null) { - forceRestart(null, true, false) - } - } - - fun pauseForGood() { - forceRestart(null, false, true) - } - - fun pauseAndLogOff() { - account = null - forceRestart(null, false, true) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index f153af0263..d4820c0a7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -21,40 +21,55 @@ package com.vitorpamplona.amethyst.model import android.util.Log -import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.fasterxml.jackson.module.kotlin.readValue -import com.fonfon.kgeohash.GeoHash import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState +import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState +import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowsPerOutboxRelay +import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState +import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState +import com.vitorpamplona.amethyst.model.nip18Reposts.RepostAction +import com.vitorpamplona.amethyst.model.nip25Reactions.ReactionAction import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListState import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState -import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction +import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.BlockPeopleListState +import com.vitorpamplona.amethyst.model.nip51Lists.GeohashListState +import com.vitorpamplona.amethyst.model.nip51Lists.HashtagListState +import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState +import com.vitorpamplona.amethyst.model.nip51Lists.MuteListState +import com.vitorpamplona.amethyst.model.nip56Reports.ReportAction +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState +import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState +import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState +import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState +import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState +import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState +import com.vitorpamplona.amethyst.model.serverList.MergedServerListState +import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState +import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.MergedTopFeedAuthorListsState +import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState +import com.vitorpamplona.amethyst.model.torState.TorRelayState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState import com.vitorpamplona.amethyst.service.uploads.FileHeader -import com.vitorpamplona.amethyst.tryAndWait -import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS -import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName -import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.tor.TorType -import com.vitorpamplona.ammolite.relays.Constants -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.Relay -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.ammolite.relays.RelaySetupInfo -import com.vitorpamplona.ammolite.relays.RelaySetupInfoToConnect -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent -import com.vitorpamplona.quartz.blossom.BlossomServersEvent import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent -import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent -import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent @@ -80,13 +95,20 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.downloadFirstEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isOnion import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote -import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags +import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash @@ -97,9 +119,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.references.references -import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip02FollowList.ReadWrite -import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent @@ -110,7 +129,6 @@ import com.vitorpamplona.quartz.nip10Notes.content.findHashtags import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip17Dm.NIP17Factory -import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent @@ -125,29 +143,26 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec -import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent -import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.emojis -import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder import com.vitorpamplona.quartz.nip37Drafts.DraftEvent -import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.Response -import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent @@ -157,17 +172,13 @@ import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter +import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip68Picture.PictureMeta import com.vitorpamplona.quartz.nip68Picture.pictureIMeta import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoMeta import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId -import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId -import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryRequestEvent import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.imetas @@ -180,36 +191,34 @@ import com.vitorpamplona.quartz.nip94FileMetadata.magnet import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag -import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent -import com.vitorpamplona.quartz.utils.DualCase +import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent +import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.czeal.rfc3986.URIReference import java.math.BigDecimal import java.util.Base64 import java.util.Locale -import kotlin.coroutines.cancellation.CancellationException +import kotlin.Boolean +import kotlin.collections.filter +import kotlin.collections.flatten +import kotlin.collections.ifEmpty +import kotlin.collections.map +import kotlin.collections.plus +import kotlin.collections.toSet import kotlin.coroutines.resume @OptIn(DelicateCoroutinesApi::class) @@ -217,905 +226,124 @@ import kotlin.coroutines.resume class Account( val settings: AccountSettings = AccountSettings(KeyPair()), val signer: NostrSigner = settings.createSigner(), + val geolocationFlow: StateFlow, + val cache: LocalCache, + val client: NostrClient, val scope: CoroutineScope, ) { - companion object { - const val APP_SPECIFIC_DATA_D_TAG = "AmethystSettings" - } - - var transientHiddenUsers: MutableStateFlow> = MutableStateFlow(setOf()) - - @Immutable - class LiveFollowList( - val authors: Set = emptySet(), - val authorsPlusMe: Set, - val hashtags: Set = emptySet(), - val geotags: Set = emptySet(), - val addresses: Set = emptySet(), - ) { - val geotagScopes: Set = geotags.mapTo(mutableSetOf()) { GeohashId.toScope(it) } - val hashtagScopes: Set = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) } - } - - class FeedsBaseFlows( - val listName: String, - val peopleList: StateFlow = MutableStateFlow(NoteState(Note(" "))), - val kind3: StateFlow = MutableStateFlow(null), - val location: StateFlow = MutableStateFlow(null), - ) - - val connectToRelaysFlow = - combineTransform( - getNIP65RelayListFlow(), - getDMRelayListFlow(), - getSearchRelayListFlow(), - getPrivateOutboxRelayListFlow(), - userProfile().flow().relays.stateFlow, - ) { nip65RelayList, dmRelayList, searchRelayList, privateOutBox, userProfile -> - checkNotInMainThread() - emit( - normalizeAndCombineRelayListsWithFallbacks( - kind3RelayList = kind3Relays(), - newDMRelayEvent = dmRelayList.note.event as? ChatMessageRelayListEvent, - searchRelayEvent = searchRelayList.note.event as? SearchRelayListEvent, - privateOutboxRelayEvent = privateOutBox.note.event as? PrivateOutboxRelayListEvent, - nip65RelayEvent = nip65RelayList.note.event as? AdvertisedRelayListEvent, - ).toTypedArray(), - ) - } - - private fun normalizeAndCombineRelayListsWithFallbacks( - kind3RelayList: Array? = null, - newDMRelayEvent: ChatMessageRelayListEvent? = null, - searchRelayEvent: SearchRelayListEvent? = null, - privateOutboxRelayEvent: PrivateOutboxRelayListEvent? = null, - nip65RelayEvent: AdvertisedRelayListEvent? = null, - localRelayList: Set? = null, - ) = normalizeAndCombineRelayLists( - baseRelaySet = kind3RelayList ?: convertLocalRelays(), - newDMRelayEvent = newDMRelayEvent ?: settings.backupDMRelayList, - searchRelayEvent = searchRelayEvent ?: settings.backupSearchRelayList, - privateOutboxRelayEvent = privateOutboxRelayEvent ?: settings.backupPrivateHomeRelayList, - nip65RelayEvent = nip65RelayEvent ?: settings.backupNIP65RelayList, - localRelayList = localRelayList ?: settings.localRelayServers, - ) - - private fun normalizeAndCombineRelayLists( - baseRelaySet: Array, - newDMRelayEvent: ChatMessageRelayListEvent?, - searchRelayEvent: SearchRelayListEvent?, - privateOutboxRelayEvent: PrivateOutboxRelayListEvent?, - nip65RelayEvent: AdvertisedRelayListEvent?, - localRelayList: Set, - ): List { - val newDMRelaySet = newDMRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet() - val searchRelaySet = (searchRelayEvent?.relays() ?: DefaultSearchRelayList).map { RelayUrlFormatter.normalize(it) }.toSet() - val nip65RelaySet = - nip65RelayEvent?.relays()?.map { - AdvertisedRelayListEvent.AdvertisedRelayInfo( - RelayUrlFormatter.normalize(it.relayUrl), - it.type, - ) - } - val privateOutboxRelaySet = privateOutboxRelayEvent?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet() - val localRelaySet = localRelayList.map { RelayUrlFormatter.normalize(it) }.toSet() - - return combineRelayLists( - baseRelaySet = baseRelaySet, - newDMRelaySet = newDMRelaySet, - searchRelaySet = searchRelaySet, - privateOutboxRelaySet = privateOutboxRelaySet, - nip65RelaySet = nip65RelaySet, - localRelaySet = localRelaySet, - ) - } - - private fun combineRelayLists( - baseRelaySet: Array, - newDMRelaySet: Set, - searchRelaySet: Set, - privateOutboxRelaySet: Set, - nip65RelaySet: List?, - localRelaySet: Set, - ): List { - // ------ - // DMs - // ------ - var mappedRelaySet = - baseRelaySet.map { - if (newDMRelaySet.contains(it.url)) { - RelaySetupInfo(it.url, true, true, it.feedTypes + FeedType.PRIVATE_DMS) - } else { - it - } - } - - newDMRelaySet.forEach { newUrl -> - if (mappedRelaySet.none { it.url == newUrl }) { - mappedRelaySet = mappedRelaySet + - RelaySetupInfo( - newUrl, - true, - true, - setOf( - FeedType.PRIVATE_DMS, - ), - ) - } - } - - // ------ - // SEARCH - // ------ - - mappedRelaySet = - mappedRelaySet.map { - if (searchRelaySet.contains(it.url)) { - RelaySetupInfo(it.url, true, it.write || false, it.feedTypes + FeedType.SEARCH) - } else { - it - } - } - - searchRelaySet.forEach { newUrl -> - if (mappedRelaySet.none { it.url == newUrl }) { - mappedRelaySet = mappedRelaySet + - RelaySetupInfo( - newUrl, - true, - false, - setOf( - FeedType.SEARCH, - ), - ) - } - } - - // -------------- - // PRIVATE OUTBOX - // -------------- - - mappedRelaySet = - mappedRelaySet.map { - if (privateOutboxRelaySet.contains(it.url)) { - RelaySetupInfo(it.url, true, true, it.feedTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS)) - } else { - it - } - } - - privateOutboxRelaySet.forEach { newUrl -> - if (mappedRelaySet.none { it.url == newUrl }) { - mappedRelaySet = mappedRelaySet + - RelaySetupInfo( - newUrl, - true, - true, - setOf( - FeedType.FOLLOWS, - FeedType.PUBLIC_CHATS, - FeedType.GLOBAL, - FeedType.PRIVATE_DMS, - ), - ) - } - } - - // -------------- - // Local Storage - // -------------- - - mappedRelaySet = - mappedRelaySet.map { - if (localRelaySet.contains(it.url)) { - RelaySetupInfo(it.url, true, true, it.feedTypes + setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL, FeedType.PRIVATE_DMS)) - } else { - it - } - } - - localRelaySet.forEach { newUrl -> - if (mappedRelaySet.none { it.url == newUrl }) { - mappedRelaySet = mappedRelaySet + - RelaySetupInfo( - newUrl, - true, - true, - setOf( - FeedType.FOLLOWS, - FeedType.PUBLIC_CHATS, - FeedType.GLOBAL, - FeedType.PRIVATE_DMS, - ), - ) - } - } - - // -------------- - // NIP-65 Public Inbox/Outbox - // -------------- - - mappedRelaySet = - mappedRelaySet.map { relay -> - val nip65setup = nip65RelaySet?.firstOrNull { relay.url == it.relayUrl } - if (nip65setup != null) { - val write = nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || nip65setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.WRITE - - RelaySetupInfo( - relay.url, - true, - relay.write || write, - relay.feedTypes + - setOf( - FeedType.FOLLOWS, - FeedType.GLOBAL, - FeedType.PUBLIC_CHATS, - ), - ) - } else { - relay - } - } - - nip65RelaySet?.forEach { newNip65Setup -> - if (mappedRelaySet.none { it.url == newNip65Setup.relayUrl }) { - val write = newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.BOTH || newNip65Setup.type == AdvertisedRelayListEvent.AdvertisedRelayType.WRITE - - mappedRelaySet = mappedRelaySet + - RelaySetupInfo( - newNip65Setup.relayUrl, - true, - write, - setOf( - FeedType.FOLLOWS, - FeedType.PUBLIC_CHATS, - ), - ) - } - } - return mappedRelaySet - } - - val connectToRelays = - connectToRelaysFlow - .flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - normalizeAndCombineRelayListsWithFallbacks( - kind3Relays(), - getDMRelayList(), - getSearchRelayList(), - getPrivateOutboxRelayList(), - getNIP65RelayList(), - ).toTypedArray(), - ) - - val connectToRelaysWithProxy = - combineTransform( - connectToRelays, - settings.torSettings.torType, - settings.torSettings.onionRelaysViaTor, - settings.torSettings.trustedRelaysViaTor, - ) { relays, torType, useTorForOnionRelays, useTorForTrustedRelays -> - emit( - relays - .map { - RelaySetupInfoToConnect( - it.url, - torType != TorType.OFF && checkLocalHostOnionAndThen(it.url, useTorForOnionRelays, useTorForTrustedRelays), - it.read, - it.write, - it.feedTypes, - ) - }.toTypedArray(), - ) - }.flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - normalizeAndCombineRelayListsWithFallbacks( - kind3Relays(), - getDMRelayList(), - getSearchRelayList(), - getPrivateOutboxRelayList(), - getNIP65RelayList(), - ).map { - RelaySetupInfoToConnect( - it.url, - settings.torSettings.torType.value != TorType.OFF && - checkLocalHostOnionAndThen( - it.url, - settings.torSettings.onionRelaysViaTor.value, - settings.torSettings.trustedRelaysViaTor.value, - ), - it.read, - it.write, - it.feedTypes, - ) - }.toTypedArray(), - ) - - fun buildFollowLists(latestContactList: ContactListEvent?): LiveFollowList { - // makes sure the output include only valid p tags - val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet() - - return LiveFollowList( - authors = verifiedFollowingUsers, - authorsPlusMe = verifiedFollowingUsers + signer.pubKey, - hashtags = - latestContactList - ?.unverifiedFollowTagSet() - ?.map { it.lowercase() } - ?.toSet() ?: emptySet(), - geotags = - latestContactList - ?.geohashes() - ?.toSet() ?: emptySet(), - addresses = - latestContactList - ?.verifiedFollowAddressSet() - ?.toSet() ?: emptySet(), - ) - } - - fun normalizeDMRelayListWithBackup(note: Note): Set { - val event = note.event as? ChatMessageRelayListEvent ?: settings.backupDMRelayList - return event?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet() - } - - val normalizedDmRelaySet = - getDMRelayListFlow() - .map { normalizeDMRelayListWithBackup(it.note) } - .flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - normalizeDMRelayListWithBackup(getDMRelayListNote()), - ) - - fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set { - val event = note.event as? PrivateOutboxRelayListEvent ?: settings.backupPrivateHomeRelayList - return event?.relays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet() - } - - val normalizedPrivateOutBoxRelaySet = - getPrivateOutboxRelayListFlow() - .map { normalizePrivateOutboxRelayListWithBackup(it.note) } - .flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote()), - ) - - fun normalizeNIP65WriteRelayListWithBackup(note: Note): Set { - val event = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList - return event?.writeRelays()?.map { RelayUrlFormatter.normalize(it) }?.toSet() ?: emptySet() - } - - val normalizedNIP65WriteRelayList = - getNIP65RelayListFlow() - .map { normalizeNIP65WriteRelayListWithBackup(it.note) } - .flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - normalizeNIP65WriteRelayListWithBackup(getNIP65RelayListNote()), - ) - - @OptIn(ExperimentalCoroutinesApi::class) - val liveKind3FollowsFlow: Flow = - userProfile().flow().follows.stateFlow.transformLatest { - emit(buildFollowLists(it.user.latestContactList)) - } - - val liveKind3Follows = - liveKind3FollowsFlow - .flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - buildFollowLists(userProfile().latestContactList ?: settings.backupContactList), - ) - - fun loadFlowsFor(listName: String): FeedsBaseFlows = - when (listName) { - GLOBAL_FOLLOWS -> FeedsBaseFlows(listName) - KIND3_FOLLOWS -> FeedsBaseFlows(listName, kind3 = liveKind3Follows) - AROUND_ME -> - FeedsBaseFlows( - listName, - location = Amethyst.instance.locationManager.geohashStateFlow, - ) - else -> { - val note = LocalCache.checkGetOrCreateAddressableNote(listName) - if (note != null) { - FeedsBaseFlows( - listName, - peopleList = - note - .flow() - .metadata.stateFlow, - ) - } else { - FeedsBaseFlows(listName) - } - } - } - - fun compute50kmLine(geoHash: GeoHash): List { - val hashes = mutableListOf() - - hashes.add(geoHash.toString()) - - var currentGeoHash = geoHash - repeat(5) { - currentGeoHash = currentGeoHash.westernNeighbour - hashes.add(currentGeoHash.toString()) - } - - currentGeoHash = geoHash - repeat(5) { - currentGeoHash = currentGeoHash.easternNeighbour - hashes.add(currentGeoHash.toString()) - } - - return hashes - } - - fun compute50kmRange(geoHash: GeoHash): List { - val hashes = mutableListOf() - - hashes.addAll(compute50kmLine(geoHash)) - - var currentGeoHash = geoHash - repeat(5) { - currentGeoHash = currentGeoHash.northernNeighbour - hashes.addAll(compute50kmLine(currentGeoHash)) - } - - currentGeoHash = geoHash - repeat(5) { - currentGeoHash = currentGeoHash.southernNeighbour - hashes.addAll(compute50kmLine(currentGeoHash)) - } - - return hashes - } - - suspend fun mapIntoFollowLists( - listName: String, - kind3: LiveFollowList?, - noteState: NoteState, - location: LocationState.LocationResult?, - ): LiveFollowList? = - if (listName == GLOBAL_FOLLOWS) { - null - } else if (listName == KIND3_FOLLOWS) { - kind3 - } else if (listName == AROUND_ME) { - val geohashResult = location ?: Amethyst.instance.locationManager.geohashStateFlow.value - if (geohashResult is LocationState.LocationResult.Success) { - // 2 neighbors deep = 25x25km - LiveFollowList( - authorsPlusMe = setOf(signer.pubKey), - geotags = compute50kmRange(geohashResult.geoHash).toSet(), - ) - } else { - LiveFollowList(authorsPlusMe = setOf(signer.pubKey)) - } - } else { - val noteEvent = noteState.note.event - if (noteEvent is GeneralListEvent) { - waitToDecrypt(noteEvent) ?: LiveFollowList(authorsPlusMe = setOf(signer.pubKey)) - } else if (noteEvent is FollowListEvent) { - LiveFollowList(authors = noteEvent.pubKeys().toSet(), authorsPlusMe = setOf(signer.pubKey) + noteEvent.pubKeys()) - } else { - LiveFollowList(authorsPlusMe = setOf(signer.pubKey)) - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - fun combinePeopleListFlows(peopleListFollowsSource: Flow): Flow = - peopleListFollowsSource - .transformLatest { listName -> - val followList = loadFlowsFor(listName) - emitAll( - combine(followList.kind3, followList.peopleList, followList.location) { kind3, peopleList, location -> - mapIntoFollowLists(followList.listName, kind3, peopleList, location) - }, - ) - } - - val liveHomeFollowLists: StateFlow by lazy { - combinePeopleListFlows(settings.defaultHomeFollowList) - .flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - runBlocking { - loadAndCombineFlows(settings.defaultHomeFollowList.value) - }, - ) - } - - val liveServerList: StateFlow> by lazy { - combine(getFileServersListFlow(), getBlossomServersListFlow()) { nip96, blossom -> - mergeServerList(nip96.note.event as? FileServersEvent, blossom.note.event as? BlossomServersEvent) - }.flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - runBlocking { - mergeServerList(getFileServersList(), getBlossomServersList()) - }, - ) - } - - suspend fun loadAndCombineFlows(listName: String): LiveFollowList? { - val flows = loadFlowsFor(listName) - return mapIntoFollowLists( - flows.listName, - flows.kind3.value, - flows.peopleList.value, - flows.location.value, - ) - } - - /** - * filter onion and local host from write relays - * for each user pubkey, a list of valid relays. - */ - private fun assembleAuthorsPerWriteRelay( - userList: Map>, - hasOnionConnection: Boolean = false, - ): Map> { - checkNotInMainThread() - - val authorsPerRelayUrl = mutableMapOf>() - val relayUrlsPerAuthor = mutableMapOf>() - - userList.forEach { userWriteRelayListPair -> - userWriteRelayListPair.value.forEach { relayUrl -> - if (!RelayUrlFormatter.isLocalHost(relayUrl) && (hasOnionConnection || !RelayUrlFormatter.isOnion(relayUrl))) { - RelayUrlFormatter.normalizeOrNull(relayUrl)?.let { normRelayUrl -> - val userSet = authorsPerRelayUrl[normRelayUrl] - if (userSet != null) { - userSet.add(userWriteRelayListPair.key) - } else { - authorsPerRelayUrl[normRelayUrl] = mutableSetOf(userWriteRelayListPair.key) - } - - val relaySet = authorsPerRelayUrl[userWriteRelayListPair.key] - if (relaySet != null) { - relaySet.add(normRelayUrl) - } else { - relayUrlsPerAuthor[userWriteRelayListPair.key] = mutableSetOf(normRelayUrl) - } - } - } - } - } - - // for each relay, authors that only use this relay go first. - // then keeps order by pubkey asc - val comparator = compareByDescending { relayUrlsPerAuthor[it]?.size ?: 0 }.thenBy { it } - - return authorsPerRelayUrl.mapValues { - it.value.sortedWith(comparator) - } - } - - fun authorsPerRelay( - followsNIP65RelayLists: List, - defaultRelayList: List, - torType: TorType, - ): Map> = authorsPerRelay(followsNIP65RelayLists, defaultRelayList, torType != TorType.OFF) - - fun authorsPerRelay( - followsNIP65RelayLists: List, - defaultRelayList: List, - acceptOnion: Boolean, - ): Map> { - checkNotInMainThread() - - val defaultSet = defaultRelayList.toSet() - - return assembleAuthorsPerWriteRelay( - followsNIP65RelayLists - .mapNotNull - { - val author = (it as? AddressableNote)?.address?.pubKeyHex - val event = (it.event as? AdvertisedRelayListEvent) - - if (event != null) { - val authorWriteRelays = - event.writeRelays().map { - RelayUrlFormatter.normalize(it) - } - - val commonRelaysToMe = authorWriteRelays.filter { it in defaultSet } - if (commonRelaysToMe.isNotEmpty()) { - event.pubKey to commonRelaysToMe - } else { - event.pubKey to defaultRelayList - } - } else { - if (author != null) { - author to defaultRelayList - } else { - Log.e("Account", "This author should NEVER be null. Note: ${it.idHex}") - null - } - } - }.toMap(), - hasOnionConnection = acceptOnion, - ) - } - - @OptIn(ExperimentalCoroutinesApi::class) - val liveHomeFollowListAdvertizedRelayListFlow: Flow?> = - liveHomeFollowLists - .transformLatest { followList -> - if (followList != null) { - emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it }) - } else { - emit(null) - } - } - - val liveHomeListAuthorsPerRelayFlow: Flow>?> by lazy { - combineTransform(liveHomeFollowListAdvertizedRelayListFlow, connectToRelays, settings.torSettings.torType) { adverisedRelayList, existing, torStatus -> - if (adverisedRelayList != null) { - emit( - authorsPerRelay( - adverisedRelayList.map { it.note }, - existing.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url }, - torStatus, - ), - ) - } else { - emit(null) - } - } - } - - val liveHomeListAuthorsPerRelay: StateFlow>?> by lazy { - liveHomeListAuthorsPerRelayFlow - .flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - authorsPerRelay( - liveHomeFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(), - connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url }, - settings.torSettings.torType.value, - ).ifEmpty { null }, - ) - } - - val liveNotificationFollowLists: StateFlow by lazy { - combinePeopleListFlows(settings.defaultNotificationFollowList) - .flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - runBlocking { - loadAndCombineFlows(settings.defaultNotificationFollowList.value) - }, - ) - } - - val liveStoriesFollowLists: StateFlow by lazy { - combinePeopleListFlows(settings.defaultStoriesFollowList) - .flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - runBlocking { - loadAndCombineFlows(settings.defaultStoriesFollowList.value) - }, - ) - } - - @OptIn(ExperimentalCoroutinesApi::class) - val liveStoriesFollowListAdvertizedRelayListFlow: Flow?> = - liveStoriesFollowLists - .transformLatest { followList -> - if (followList != null) { - emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it }) - } else { - emit(null) - } - } - - val liveStoriesListAuthorsPerRelayFlow: Flow>?> by lazy { - combineTransform(liveStoriesFollowListAdvertizedRelayListFlow, connectToRelays, settings.torSettings.torType) { adverisedRelayList, existing, torState -> - if (adverisedRelayList != null) { - emit( - authorsPerRelay( - adverisedRelayList.map { it.note }, - existing.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url }, - torState, - ), - ) - } else { - emit(null) - } - } - } - - val liveStoriesListAuthorsPerRelay: StateFlow>?> by lazy { - liveStoriesListAuthorsPerRelayFlow.flowOn(Dispatchers.Default).stateIn( + private var userProfileCache: User? = null + + fun userProfile(): User = userProfileCache ?: cache.getOrCreateUser(signer.pubKey).also { userProfileCache = it } + + val userMetadata = UserMetadataState(signer, cache, scope, settings) + + val nip65RelayList = Nip65RelayListState(signer, cache, scope, settings) + val dmRelayList = DmRelayListState(signer, cache, scope, settings) + val searchRelayList = SearchRelayListState(signer, cache, scope, settings) + val privateStorageRelayList = PrivateStorageRelayListState(signer, cache, scope, settings) + val localRelayList = LocalRelayListState(signer, cache, scope, settings) + + val kind3FollowList = FollowListState(signer, cache, scope, settings) + val ephemeralChatList = EphemeralChatListState(signer, cache, scope, settings) + val publicChatList = PublicChatListState(signer, cache, scope, settings) + val communityList = CommunityListState(signer, cache, scope, settings) + val hashtagList = HashtagListState(signer, cache, scope, settings) + val geohashList = GeohashListState(signer, cache, scope, settings) + + val muteList = MuteListState(signer, cache, scope, settings) + val blockPeopleList = BlockPeopleListState(signer, cache, scope) + val hiddenUsers = HiddenUsersState(muteList.flow, blockPeopleList.flow, scope, settings) + + val emoji = EmojiPackState(signer, cache, scope) + + val appSpecific = AppSpecificState(signer, cache, scope, settings) + + val blossomServers = BlossomServerListState(signer, cache, scope, settings) + val fileStorageServers = FileStorageServerListState(signer, cache, scope, settings) + val serverLists = MergedServerListState(fileStorageServers.fileServers, blossomServers.fileServers, scope) + + // Relay settings + val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, scope) + val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope) + val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope) + + val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, scope) + + // Follows Relays + val followOutboxes = FollowListOutboxRelays(kind3FollowList, cache, scope) + val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, scope) + + // keeps a cache of the outbox relays for each author + val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, cache, scope).flow + + // Merges all follow lists to create a single All Follows feed. + val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope) + + // App-ready Feeds + val liveHomeFollowLists: StateFlow = + FeedTopNavFilterState( + feedFilterListName = settings.defaultHomeFollowList, + allFollows = allFollows.flow, + locationFlow = geolocationFlow, + followsRelays = followPlusAllMine.flow, + signer = signer, + scope = scope, + ).flow + + val liveHomeFollowListsPerRelay = OutboxLoaderState(liveHomeFollowLists, cache, scope).flow + + val liveStoriesFollowLists: StateFlow = + FeedTopNavFilterState( + feedFilterListName = settings.defaultStoriesFollowList, + allFollows = allFollows.flow, + locationFlow = geolocationFlow, + followsRelays = followPlusAllMine.flow, + signer = signer, + scope = scope, + ).flow + + val liveStoriesFollowListsPerRelay = OutboxLoaderState(liveStoriesFollowLists, cache, scope).flow + + val liveDiscoveryFollowLists: StateFlow = + FeedTopNavFilterState( + feedFilterListName = settings.defaultDiscoveryFollowList, + allFollows = allFollows.flow, + locationFlow = geolocationFlow, + followsRelays = followPlusAllMine.flow, + signer = signer, + scope = scope, + ).flow + + val liveDiscoveryFollowListsPerRelay = OutboxLoaderState(liveDiscoveryFollowLists, cache, scope).flow + + val liveNotificationFollowLists: StateFlow = + FeedTopNavFilterState( + feedFilterListName = settings.defaultNotificationFollowList, + allFollows = allFollows.flow, + locationFlow = geolocationFlow, + followsRelays = followPlusAllMine.flow, + signer = signer, + scope = scope, + ).flow + + val liveNotificationFollowListsPerRelay = OutboxLoaderState(liveNotificationFollowLists, cache, scope).flow + + val mergedTopFeedAuthorLists = + MergedTopFeedAuthorListsState( + liveHomeFollowListsPerRelay, + liveStoriesFollowListsPerRelay, + liveDiscoveryFollowListsPerRelay, + liveNotificationFollowListsPerRelay, scope, - SharingStarted.Eagerly, - authorsPerRelay( - liveStoriesFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(), - connectToRelays.value.filter { it.feedTypes.contains(FeedType.FOLLOWS) && it.read }.map { it.url }, - settings.torSettings.torType.value, - ).ifEmpty { null }, - ) - } + ).flow - val liveDiscoveryFollowLists: StateFlow by lazy { - combinePeopleListFlows(settings.defaultDiscoveryFollowList) - .flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - runBlocking { - loadAndCombineFlows(settings.defaultDiscoveryFollowList.value) - }, - ) - } - - @OptIn(ExperimentalCoroutinesApi::class) - val liveDiscoveryFollowListAdvertizedRelayListFlow: Flow?> = - liveDiscoveryFollowLists - .transformLatest { followList -> - if (followList != null) { - emitAll(combine(followList.authorsPlusMe.map { getNIP65RelayListFlow(it) }) { it }) - } else { - emit(null) - } - } - - val liveDiscoveryListAuthorsPerRelayFlow: Flow>?> by lazy { - combineTransform(liveDiscoveryFollowListAdvertizedRelayListFlow, connectToRelays, settings.torSettings.torType) { adverisedRelayList, existing, torState -> - if (adverisedRelayList != null) { - emit( - authorsPerRelay( - adverisedRelayList.map { it.note }, - existing.filter { it.read }.map { it.url }, - torState, - ), - ) - } else { - emit(null) - } - } - } - - val liveDiscoveryListAuthorsPerRelay: StateFlow>?> by lazy { - liveDiscoveryListAuthorsPerRelayFlow.flowOn(Dispatchers.Default).stateIn( - scope, - SharingStarted.Eagerly, - authorsPerRelay( - liveDiscoveryFollowLists.value?.authorsPlusMe?.map { getNIP65RelayListNote(it) } ?: emptyList(), - connectToRelays.value.filter { it.read }.map { it.url }, - settings.torSettings.torType.value, - ).ifEmpty { null }, - ) - } - - private fun decryptLiveFollows( - listEvent: GeneralListEvent, - onReady: (LiveFollowList) -> Unit, - ) { - listEvent.privateTags(signer) { privateTagList -> - val users = (listEvent.taggedUserIds() + listEvent.filterUsers(privateTagList)).toSet() - onReady( - LiveFollowList( - authors = users, - authorsPlusMe = users + userProfile().pubkeyHex, - hashtags = (listEvent.hashtags() + listEvent.filterHashtags(privateTagList)).toSet(), - geotags = (listEvent.geohashes() + listEvent.filterGeohashes(privateTagList)).toSet(), - addresses = - (listEvent.taggedATags() + listEvent.filterATags(privateTagList)) - .map { it.toTag() } - .toSet(), - ), - ) - } - } + val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope) fun decryptPeopleList( event: GeneralListEvent, onReady: (Array>) -> Unit, ) = event.privateTags(signer, onReady) - suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowList? = - tryAndWait { continuation -> - decryptLiveFollows(peopleListFollows) { - continuation.resume(it) - } - } - - @Immutable - class LiveHiddenUsers( - val hiddenUsers: Set, - val spammers: Set, - val hiddenWords: Set, - val showSensitiveContent: Boolean?, - ) { - // speeds up isHidden calculations - val hiddenUsersHashCodes = hiddenUsers.mapTo(HashSet()) { it.hashCode() } - val spammersHashCodes = spammers.mapTo(HashSet()) { it.hashCode() } - val hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) } - } - - suspend fun decryptPeopleList(event: PeopleListEvent?): PeopleListEvent.UsersAndWords { - if (event == null || !isWriteable()) return PeopleListEvent.UsersAndWords() - - return tryAndWait { continuation -> - event.publicAndPrivateUsersAndWords(signer) { - continuation.resume(it) - } - } ?: PeopleListEvent.UsersAndWords() - } - - suspend fun decryptMuteList(event: MuteListEvent?): PeopleListEvent.UsersAndWords { - if (event == null || !isWriteable()) return PeopleListEvent.UsersAndWords() - - return tryAndWait { continuation -> - event.publicAndPrivateUsersAndWords(signer) { - continuation.resume(it) - } - } ?: PeopleListEvent.UsersAndWords() - } - - suspend fun assembleLiveHiddenUsers( - blockList: Note, - muteList: Note, - transientHiddenUsers: Set, - showSensitiveContent: Boolean?, - ): LiveHiddenUsers { - val resultBlockList = decryptPeopleList(blockList.event as? PeopleListEvent) - val resultMuteList = decryptMuteList(muteList.event as? MuteListEvent) - - return LiveHiddenUsers( - hiddenUsers = resultBlockList.users + resultMuteList.users, - hiddenWords = resultBlockList.words + resultMuteList.words, - spammers = transientHiddenUsers, - showSensitiveContent = showSensitiveContent, - ) - } - - val flowHiddenUsers: StateFlow by lazy { - combineTransform( - getBlockListNote().flow().metadata.stateFlow, - getMuteListNote().flow().metadata.stateFlow, - transientHiddenUsers, - settings.syncedSettings.security.showSensitiveContent, - ) { blockList, muteList, transientHiddenUsers, showSensitiveContent -> - checkNotInMainThread() - emit(assembleLiveHiddenUsers(blockList.note, muteList.note, transientHiddenUsers, showSensitiveContent)) - }.flowOn(Dispatchers.Default) - .stateIn( - scope, - SharingStarted.Eagerly, - runBlocking { - assembleLiveHiddenUsers( - getBlockListNote(), - getMuteListNote(), - transientHiddenUsers.value, - settings.syncedSettings.security.showSensitiveContent.value, - ) - }, - ) - } - @OptIn(FlowPreview::class) val decryptBookmarks: Flow by lazy { userProfile() @@ -1138,14 +366,6 @@ class Account( .flowOn(Dispatchers.Default) } - val emoji = EmojiPackState(signer, LocalCache, scope) - val ephemeralChatList = EphemeralChatListState(signer, LocalCache, scope) - val publicChatList = PublicChatListState(signer, LocalCache, scope) - - private var userProfileCache: User? = null - - fun userProfile(): User = userProfileCache ?: LocalCache.getOrCreateUser(signer.pubKey).also { userProfileCache = it } - fun isWriteable(): Boolean = settings.isWriteable() fun updateWarnReports(warnReports: Boolean): Boolean { @@ -1159,9 +379,7 @@ class Account( fun updateFilterSpam(filterSpam: Boolean): Boolean { if (settings.updateFilterSpam(filterSpam)) { if (!settings.syncedSettings.security.filterSpamFromStrangers.value) { - transientHiddenUsers.update { - emptySet() - } + hiddenUsers.resetTransientUsers() } sendNewAppSpecificData() @@ -1185,7 +403,7 @@ class Account( fun updateZapAmounts( amountSet: List, selectedZapType: LnZapEvent.ZapType, - nip47Update: Nip47WalletConnect.Nip47URI?, + nip47Update: Nip47WalletConnect.Nip47URINorm?, ) { var changed = false @@ -1219,54 +437,12 @@ class Account( } private fun sendNewAppSpecificData() { - sendNewAppSpecificData(settings.syncedSettings.toInternal()) - } - - private fun sendNewAppSpecificData(toInternal: AccountSyncedSettingsInternal) { - signer.nip44Encrypt(EventMapper.mapper.writeValueAsString(toInternal), signer.pubKey) { encrypted -> - AppSpecificDataEvent.create( - dTag = APP_SPECIFIC_DATA_D_TAG, - description = encrypted, - otherTags = emptyArray(), - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - } - - fun sendKind3RelayList(relays: Map) { if (!isWriteable()) return - val contactList = userProfile().latestContactList - - if (contactList != null && contactList.tags.isNotEmpty()) { - ContactListEvent.updateRelayList( - earlierVersion = contactList, - relayUse = relays, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - ContactListEvent.createFromScratch( - followUsers = listOf(), - followTags = listOf(), - followGeohashes = listOf(), - followCommunities = listOf(), - relayUse = relays, - signer = signer, - ) { - // Keep this local to avoid erasing a good contact list. - // Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + appSpecific.saveNewAppSpecificData(::sendMyPublicAndPrivateOutbox) } - suspend fun countFollowersOf(pubkey: HexKey): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkey) ?: false } + suspend fun countFollowersOf(pubkey: HexKey): Int = cache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkey) ?: false } suspend fun followerCount(): Int = countFollowersOf(signer.pubKey) @@ -1286,48 +462,10 @@ class Account( ) { if (!isWriteable()) return - val latest = userProfile().latestMetadata - - val template = - if (latest != null) { - MetadataEvent.updateFromPast( - latest = latest, - name = name, - displayName = name, - picture = picture, - banner = banner, - website = website, - pronouns = pronouns, - about = about, - nip05 = nip05, - lnAddress = lnAddress, - lnURL = lnURL, - twitter = twitter, - mastodon = mastodon, - github = github, - ) - } else { - MetadataEvent.createNew( - name = name, - displayName = name, - picture = picture, - banner = banner, - website = website, - pronouns = pronouns, - about = about, - nip05 = nip05, - lnAddress = lnAddress, - lnURL = lnURL, - twitter = twitter, - mastodon = mastodon, - github = github, - ) - } - - signer.sign(template) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } + userMetadata.sendNewUserMetadata( + name, picture, banner, website, pronouns, about, nip05, lnAddress, lnURL, twitter, mastodon, github, + ::sendMyPublicAndPrivateOutbox, + ) } fun reactionTo( @@ -1347,74 +485,17 @@ class Account( suspend fun reactTo( note: Note, reaction: String, - ) { - if (!isWriteable()) return - - if (hasReacted(note, reaction)) { - // has already liked this note - return - } - - val noteEvent = note.event - if (noteEvent is NIP17Group) { - val users = noteEvent.groupMembers().toList() - - if (reaction.startsWith(":")) { - val emojiUrl = EmojiUrlTag.decode(reaction) - if (emojiUrl != null) { - note.toEventHint()?.let { - NIP17Factory().createReactionWithinGroup( - emojiUrl = emojiUrl, - originalNote = it, - to = users, - signer = signer, - ) { - broadcastPrivately(it) - } - } - - return - } - } - - note.toEventHint()?.let { - NIP17Factory().createReactionWithinGroup( - content = reaction, - originalNote = it, - to = users, - signer = signer, - ) { - broadcastPrivately(it) - } - } - return - } else { - if (reaction.startsWith(":")) { - val emojiUrl = EmojiUrlTag.decode(reaction) - if (emojiUrl != null) { - note.event?.let { - signer.sign( - ReactionEvent.build(emojiUrl, EventHintBundle(it, note.relayHintUrl())), - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - - return - } - } - - note.toEventHint()?.let { - signer.sign( - ReactionEvent.build(reaction, it), - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - } - } + ) = ReactionAction.reactTo( + note, + reaction, + userProfile(), + signer, + onPublic = { + client.send(it, computeMyReactionToNote(note, it)) + cache.justConsumeMyOwnEvent(it) + }, + onPrivate = ::broadcastPrivately, + ) fun createZapRequestFor( note: Note, @@ -1422,15 +503,17 @@ class Account( message: String = "", zapType: LnZapEvent.ZapType, toUser: User?, - additionalRelays: Set? = null, + additionalRelays: Set? = null, onReady: (LnZapRequestEvent) -> Unit, ) { if (!isWriteable()) return + val relays = nip65RelayList.inboxFlow.value + (additionalRelays ?: emptySet()) + note.event?.let { event -> LnZapRequestEvent.create( event, - relays = getReceivingRelays() + (additionalRelays ?: emptySet()), + relays = relays.mapTo(mutableSetOf()) { it.url }, signer, pollOption, message, @@ -1441,19 +524,6 @@ class Account( } } - fun getReceivingRelays(): Set = - getNIP65RelayList()?.readRelays()?.toSet() - ?: userProfile() - .latestContactList - ?.relays() - ?.filter { it.value.read } - ?.keys - ?.ifEmpty { null } - ?: settings.localRelays - .filter { it.read } - .map { it.url } - .toSet() - fun hasWalletConnectSetup(): Boolean = settings.zapPaymentRequest != null fun isNIP47Author(pubkeyHex: String?): Boolean = (getNIP47Signer().pubKey == pubkeyHex) @@ -1509,28 +579,21 @@ class Account( fromServiceHex = nip47.pubKeyHex, toUserHex = event.pubKey, replyingToHex = event.id, + relay = nip47.relayUri, ) - Amethyst.instance.sources.nwc - .subscribe(filter) + Amethyst.instance.sources.nwc.subscribe(filter) - LocalCache.consume(event, zappedNote, true) { it.response(signer) { onResponse(it) } } + GlobalScope.launch(Dispatchers.IO) { + delay(60000) // waits 1 minute to complete payment. + Amethyst.instance.sources.nwc.unsubscribe(filter) + } - Amethyst.instance.client.sendSingle( - signedEvent = event, - relayTemplate = - RelaySetupInfoToConnect( - url = nip47.relayUri, - forceProxy = shouldUseTorForTrustedRelays(), - read = true, - write = true, - feedTypes = setOf(FeedType.WALLET_CONNECT), - ), - onDone = { - Amethyst.instance.sources.nwc - .unsubscribe(filter) - }, - ) + cache.consume(event, zappedNote, true, nip47.relayUri) { + it.response(signer) { onResponse(it) } + } + + client.send(event, setOf(nip47.relayUri)) onSent() } @@ -1545,12 +608,7 @@ class Account( ) { LnZapRequestEvent.create( userPubKeyHex, - userProfile() - .latestContactList - ?.relays() - ?.keys - ?.ifEmpty { null } - ?: settings.localRelays.map { it.url }.toSet(), + nip65RelayList.inboxFlow.value.toSet(), signer, message, zapType, @@ -1562,39 +620,12 @@ class Account( note: Note, type: ReportType, content: String = "", - ) { - if (!isWriteable()) return - - if (note.hasReport(userProfile(), type)) { - // has already reported this note - return - } - - note.event?.let { - signer.sign(ReportEvent.build(it, type)) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - } + ) = ReportAction.report(note, type, content, userProfile(), signer, ::sendMyPublicAndPrivateOutbox) suspend fun report( user: User, type: ReportType, - ) { - if (!isWriteable()) return - - if (user.hasReport(userProfile(), type)) { - // has already reported this note - return - } - - val template = ReportEvent.build(user.pubkeyHex, type) - signer.sign(template) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + ) = ReportAction.report(user, type, userProfile(), signer, ::sendMyPublicAndPrivateOutbox) fun delete(note: Note) { delete(listOf(note)) @@ -1603,15 +634,20 @@ class Account( fun delete(notes: List) { if (!isWriteable()) return - val myNoteVersions = notes.filter { it.author == userProfile() }.mapNotNull { it.event as? Event } - if (myNoteVersions.isNotEmpty()) { + val myNotes = notes.filter { it.author == userProfile() && it.event != null } + if (myNotes.isNotEmpty()) { // chunks in 200 elements to avoid going over the 65KB limit for events. - myNoteVersions.chunked(200).forEach { chunkedList -> + myNotes.chunked(200).forEach { chunkedList -> signer.sign( - DeletionEvent.build(chunkedList), + DeletionEvent.build(chunkedList.mapNotNull { it.event }), ) { deletionEvent -> - Amethyst.instance.client.send(deletionEvent) - LocalCache.justConsumeMyOwnEvent(deletionEvent) + val myRelayList = outboxRelays.flow.value.toMutableSet() + chunkedList.forEach { + myRelayList.addAll(it.relays) + } + + client.send(deletionEvent, outboxRelays.flow.value + myRelayList) + cache.justConsumeMyOwnEvent(deletionEvent) } } } @@ -1661,52 +697,236 @@ class Account( } suspend fun boost(note: Note) { - if (!isWriteable()) return - val noteEvent = note.event ?: return - - if (note.hasBoostedInTheLast5Minutes(userProfile())) { - // has already bosted in the past 5mins - return + RepostAction.repost(note, signer) { + client.send(it, computeMyReactionToNote(note, it)) + cache.justConsumeMyOwnEvent(it) } + } - val noteHint = note.relayHintUrl() - val authorHint = note.author?.bestRelayHint() + fun computeMyReactionToNote( + note: Note, + reaction: Event, + ): Set { + val relaysItCameFrom = note.relays - val template = - if (noteEvent.kind == 1) { - RepostEvent.build(noteEvent, noteHint, authorHint) - } else { - GenericRepostEvent.build(noteEvent, noteHint, authorHint) + val inboxRelaysOfTheAuthorOfTheOriginalNote = + note.author?.inboxRelays() ?: note.author?.pubkeyHex?.let { + cache.relayHints.hintsForKey(it) + } ?: emptyList() + + val reactionOutBoxRelays = outboxRelays.flow.value + + val taggedUsers = reaction.taggedUserIds() + (note.event?.taggedUserIds() ?: emptyList()) + + val taggedUserInboxRelays = + taggedUsers.flatMapTo(mutableSetOf()) { pubkey -> + if (pubkey == userProfile().pubkeyHex) { + notificationRelays.flow.value + } else { + LocalCache.getUserIfExists(pubkey)?.inboxRelays()?.ifEmpty { null }?.toSet() + ?: cache.relayHints.hintsForKey(pubkey).toSet() + } } - signer.sign(template) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } + val isInChannel = note.channelHex() + val channelRelays = + if (isInChannel != null) { + val channel = LocalCache.checkGetOrCreateChannel(isInChannel) + channel?.relays() ?: emptySet() + } else { + emptySet() + } + + val replyRelays = + note.replyTo?.flatMapTo(mutableSetOf()) { + val existingRelays = it.relays.toSet() + + val replyToAuthor = it.author + + val replyAuthorRelays = + if (replyToAuthor != null) { + if (replyToAuthor == userProfile()) { + outboxRelays.flow.value + } else { + replyToAuthor.outboxRelays().ifEmpty { null }?.toSet() + ?: cache.relayHints.hintsForKey(replyToAuthor.pubkeyHex).ifEmpty { null }?.toSet() + ?: emptySet() + } + } else { + emptySet() + } + + existingRelays + replyAuthorRelays + } ?: emptySet() + + return reactionOutBoxRelays + + inboxRelaysOfTheAuthorOfTheOriginalNote + + taggedUserInboxRelays + + channelRelays + + replyRelays + + relaysItCameFrom + } + + fun computeRelayListToBroadcast(event: Event): Set { + val author = cache.getUserIfExists(event.pubKey) + + val authorOutboxRelays = + if (author != null) { + if (author == userProfile()) { + outboxRelays.flow.value + } else { + author.outboxRelays().ifEmpty { null }?.toSet() + ?: cache.relayHints.hintsForKey(author.pubkeyHex).ifEmpty { null }?.toSet() + ?: emptySet() + } + } else { + cache.relayHints.hintsForKey(event.pubKey).ifEmpty { null }?.toSet() + emptySet() + } + + val taggedUserInboxRelays = + event.taggedUserIds().flatMapTo(mutableSetOf()) { pubkey -> + if (pubkey == userProfile().pubkeyHex) { + notificationRelays.flow.value + } else { + LocalCache.getUserIfExists(pubkey)?.inboxRelays()?.ifEmpty { null }?.toSet() + ?: cache.relayHints.hintsForKey(pubkey).toSet() + } + } + + val isInChannel = + if ( + event is ChannelMessageEvent || + event is ChannelMetadataEvent || + event is ChannelCreateEvent || + event is LiveActivitiesChatMessageEvent || + event is LiveActivitiesEvent || + event is EphemeralChatEvent + ) { + (event as? ChannelMessageEvent)?.channelId() + ?: (event as? ChannelMetadataEvent)?.channelId() + ?: (event as? ChannelCreateEvent)?.id + ?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag() + ?: (event as? LiveActivitiesEvent)?.aTag()?.toTag() + ?: (event as? EphemeralChatEvent)?.roomId()?.toKey() + } else { + null + } + + val channelRelays = + if (isInChannel != null) { + val channel = LocalCache.checkGetOrCreateChannel(isInChannel) + channel?.relays() ?: emptySet() + } else { + emptySet() + } + + val replyRelays = + cache.computeReplyTo(event).flatMapTo(mutableSetOf()) { + val existingRelays = it.relays.toSet() + val replyToAuthor = it.author + + val replyAuthorRelays = + if (replyToAuthor != null) { + if (replyToAuthor == userProfile()) { + outboxRelays.flow.value + } else { + replyToAuthor.outboxRelays().ifEmpty { null }?.toSet() + ?: cache.relayHints.hintsForKey(replyToAuthor.pubkeyHex).ifEmpty { null }?.toSet() + ?: emptySet() + } + } else { + emptySet() + } + + existingRelays + replyAuthorRelays + } + + return authorOutboxRelays + taggedUserInboxRelays + channelRelays + replyRelays + } + + fun computeRelayListToBroadcast(note: Note): Set { + val author = note.author + + val authorOutboxRelays = + if (author != null) { + if (author == userProfile()) { + outboxRelays.flow.value + } else { + author.outboxRelays().ifEmpty { null }?.toSet() + ?: cache.relayHints.hintsForKey(author.pubkeyHex).ifEmpty { null }?.toSet() + ?: emptySet() + } + } else { + emptySet() + } + + val taggedUserInboxRelays = + note.event?.taggedUserIds()?.flatMapTo(mutableSetOf()) { pubkey -> + if (pubkey == userProfile().pubkeyHex) { + notificationRelays.flow.value + } else { + LocalCache.getUserIfExists(pubkey)?.inboxRelays()?.ifEmpty { null }?.toSet() + ?: cache.relayHints.hintsForKey(pubkey).toSet() + } + } ?: emptySet() + + val isInChannel = note.channelHex() + val channelRelays = + if (isInChannel != null) { + val channel = LocalCache.checkGetOrCreateChannel(isInChannel) + channel?.relays() ?: emptySet() + } else { + emptySet() + } + + val replyRelays = + note.replyTo?.flatMapTo(mutableSetOf()) { + val existingRelays = it.relays.toSet() + + val replyToAuthor = it.author + + val replyAuthorRelays = + if (replyToAuthor != null) { + if (replyToAuthor == userProfile()) { + outboxRelays.flow.value + } else { + replyToAuthor.outboxRelays().ifEmpty { null }?.toSet() + ?: cache.relayHints.hintsForKey(replyToAuthor.pubkeyHex).ifEmpty { null }?.toSet() + ?: emptySet() + } + } else { + emptySet() + } + + existingRelays + replyAuthorRelays + } ?: emptySet() + + return authorOutboxRelays + taggedUserInboxRelays + channelRelays + replyRelays } fun broadcast(note: Note) { note.event?.let { if (it is WrappedEvent && it.host != null) { // download the event and send it. - it.host?.let { - Amethyst.instance.client.sendFilterAndStopOnFirstResponse( + it.host?.let { host -> + client.downloadFirstEvent( filters = - listOf( - TypedFilter( - setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL), - SincePerRelayFilter( - ids = listOf(it.id), + note.relays.map { relay -> + RelayBasedFilter( + relay, + Filter( + ids = listOf(host.id), ), - ), - ), + ) + }, onResponse = { - Amethyst.instance.client.send(it) + client.send(it, computeRelayListToBroadcast(it)) }, ) } } else { - Amethyst.instance.client.send(it) + client.send(it, computeRelayListToBroadcast(note)) } } } @@ -1720,7 +940,7 @@ class Account( val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(pair.value), pair.key, otsResolver) if (otsState != null) { - val hint = LocalCache.getNoteIfExists(pair.key)?.toEventHint() + val hint = cache.getNoteIfExists(pair.key)?.toEventHint() val template = if (hint != null) { @@ -1730,8 +950,8 @@ class Account( } signer.sign(template) { - LocalCache.justConsumeMyOwnEvent(it) - Amethyst.instance.client.send(it) + cache.justConsumeMyOwnEvent(it) + client.send(it, computeRelayListToBroadcast(it)) settings.pendingAttestations.update { it - pair.key @@ -1756,222 +976,33 @@ class Account( settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id, otsResolver))) } - fun follow(user: User) { - if (!isWriteable()) return + fun follow(user: User) = kind3FollowList.follow(user, this::sendMyPublicAndPrivateOutbox) - val contactList = userProfile().latestContactList + fun unfollow(user: User) = kind3FollowList.unfollow(user, this::sendMyPublicAndPrivateOutbox) - if (contactList != null) { - ContactListEvent.followUser(contactList, user.pubkeyHex, signer) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - ContactListEvent.createFromScratch( - followUsers = listOf(ContactTag(user.pubkeyHex, user.bestRelayHint(), null)), - followTags = emptyList(), - followGeohashes = emptyList(), - followCommunities = emptyList(), - relayUse = - Constants.defaultRelays.associate { - it.url to ReadWrite(it.read, it.write) - }, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - } + fun follow(channel: PublicChatChannel) = publicChatList.follow(channel, ::sendToPrivateOutboxAndLocal) - fun follow(channel: PublicChatChannel) { - if (!isWriteable()) return + fun unfollow(channel: PublicChatChannel) = publicChatList.unfollow(channel, ::sendToPrivateOutboxAndLocal) - publicChatList.follow(channel) { - sendToPrivateOutboxAndLocal(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + fun follow(channel: EphemeralChatChannel) = ephemeralChatList.follow(channel, ::sendToPrivateOutboxAndLocal) - fun unfollow(channel: PublicChatChannel) { - if (!isWriteable()) return + fun unfollow(channel: EphemeralChatChannel) = ephemeralChatList.unfollow(channel, ::sendToPrivateOutboxAndLocal) - publicChatList.unfollow(channel) { - sendToPrivateOutboxAndLocal(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + fun follow(community: AddressableNote) = communityList.follow(community, ::sendToPrivateOutboxAndLocal) - fun follow(channel: EphemeralChatChannel) { - if (!isWriteable()) return + fun unfollow(community: AddressableNote) = communityList.unfollow(community, ::sendToPrivateOutboxAndLocal) - ephemeralChatList.follow(channel) { - sendToPrivateOutboxAndLocal(it) - LocalCache.justConsumeInner(it, RelayBriefInfoCache.get(channel.roomId.relayUrl), true) - } - } + fun followHashtag(tag: String) = hashtagList.follow(tag, ::sendMyPublicAndPrivateOutbox) - fun unfollow(channel: EphemeralChatChannel) { - if (!isWriteable()) return + fun unfollowHashtag(tag: String) = hashtagList.unfollow(tag, ::sendMyPublicAndPrivateOutbox) - ephemeralChatList.unfollow(channel) { - sendToPrivateOutboxAndLocal(it) - LocalCache.justConsumeInner(it, RelayBriefInfoCache.get(channel.roomId.relayUrl), true) - } - } + fun followGeohash(geohash: String) = geohashList.follow(geohash, ::sendMyPublicAndPrivateOutbox) - fun follow(community: AddressableNote) { - if (!isWriteable()) return + fun unfollowGeohash(geohash: String) = geohashList.unfollow(geohash, ::sendMyPublicAndPrivateOutbox) - val contactList = userProfile().latestContactList - - if (contactList != null) { - ContactListEvent.followAddressableEvent(contactList, community.toATag(), signer) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - val relays = - Constants.defaultRelays.associate { - it.url to ReadWrite(it.read, it.write) - } - ContactListEvent.createFromScratch( - followUsers = emptyList(), - followTags = emptyList(), - followGeohashes = emptyList(), - followCommunities = listOf(community.toATag()), - relayUse = relays, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - } - - fun followHashtag(tag: String) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - - if (contactList != null) { - ContactListEvent.followHashtag( - contactList, - tag, - signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - ContactListEvent.createFromScratch( - followUsers = emptyList(), - followTags = listOf(tag), - followGeohashes = emptyList(), - followCommunities = emptyList(), - relayUse = - Constants.defaultRelays.associate { - it.url to ReadWrite(it.read, it.write) - }, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - } - - fun followGeohash(geohash: String) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - - if (contactList != null) { - ContactListEvent.followGeohash( - contactList, - geohash, - signer, - onReady = this::onNewEventCreated, - ) - } else { - ContactListEvent.createFromScratch( - followUsers = emptyList(), - followTags = emptyList(), - followGeohashes = listOf(geohash), - followCommunities = emptyList(), - relayUse = - Constants.defaultRelays.associate { - it.url to ReadWrite(it.read, it.write) - }, - signer = signer, - onReady = this::onNewEventCreated, - ) - } - } - - fun onNewEventCreated(event: Event) { - Amethyst.instance.client.send(event) - LocalCache.justConsumeMyOwnEvent(event) - } - - fun unfollow(user: User) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - - if (contactList != null && contactList.tags.isNotEmpty()) { - ContactListEvent.unfollowUser( - contactList, - user.pubkeyHex, - signer, - onReady = this::onNewEventCreated, - ) - } - } - - suspend fun unfollowHashtag(tag: String) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - - if (contactList != null && contactList.tags.isNotEmpty()) { - ContactListEvent.unfollowHashtag( - contactList, - tag, - signer, - onReady = this::onNewEventCreated, - ) - } - } - - suspend fun unfollowGeohash(geohash: String) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - - if (contactList != null && contactList.tags.isNotEmpty()) { - ContactListEvent.unfollowGeohash( - contactList, - geohash, - signer, - onReady = this::onNewEventCreated, - ) - } - } - - suspend fun unfollow(community: AddressableNote) { - if (!isWriteable()) return - - val contactList = userProfile().latestContactList - - if (contactList != null && contactList.tags.isNotEmpty()) { - ContactListEvent.unfollowAddressableEvent( - contactList, - community.toATag(), - signer, - onReady = this::onNewEventCreated, - ) - } + fun sendMyPublicAndPrivateOutbox(event: Event) { + client.send(event, outboxRelays.flow.value) + cache.justConsumeMyOwnEvent(event) } fun createNip95( @@ -2007,68 +1038,47 @@ class Account( fun consumeAndSendNip95( data: FileStorageEvent, signedEvent: FileStorageHeaderEvent, - relayList: List, + relayList: List, ): Note? { if (!isWriteable()) return null - Amethyst.instance.client.send(data, relayList = relayList) - LocalCache.justConsumeMyOwnEvent(data) + client.send(data, relayList = relayList.toSet()) + cache.justConsumeMyOwnEvent(data) - Amethyst.instance.client.send(signedEvent, relayList = relayList) - LocalCache.justConsumeMyOwnEvent(signedEvent) + client.send(signedEvent, relayList = relayList.toSet()) + cache.justConsumeMyOwnEvent(signedEvent) - return LocalCache.getNoteIfExists(signedEvent.id) + return cache.getNoteIfExists(signedEvent.id) } fun consumeNip95( data: FileStorageEvent, signedEvent: FileStorageHeaderEvent, ): Note? { - LocalCache.justConsumeMyOwnEvent(data) - LocalCache.justConsumeMyOwnEvent(signedEvent) + cache.justConsumeMyOwnEvent(data) + cache.justConsumeMyOwnEvent(signedEvent) - return LocalCache.getNoteIfExists(signedEvent.id) + return cache.getNoteIfExists(signedEvent.id) } fun sendNip95( data: FileStorageEvent, signedEvent: FileStorageHeaderEvent, - relayList: List, + relayList: Set, ) { - Amethyst.instance.client.send(data, relayList = relayList) - Amethyst.instance.client.send(signedEvent, relayList = relayList) - } - - fun sendNip95Privately( - data: FileStorageEvent, - signedEvent: FileStorageHeaderEvent, - relayList: List, - ) { - val connect = - relayList.map { - val normalizedUrl = RelayUrlFormatter.normalize(it) - RelaySetupInfoToConnect( - normalizedUrl, - shouldUseTorForClean(normalizedUrl), - true, - true, - setOf(FeedType.GLOBAL), - ) - } - - Amethyst.instance.client.sendPrivately(data, relayList = connect) - Amethyst.instance.client.sendPrivately(signedEvent, relayList = connect) + client.send(data, relayList = relayList) + client.send(signedEvent, relayList = relayList) } fun sendHeader( signedEvent: Event, - relayList: List, + relayList: Set, onReady: (Note) -> Unit, ) { - Amethyst.instance.client.send(signedEvent, relayList = relayList) - LocalCache.justConsumeMyOwnEvent(signedEvent) + client.send(signedEvent, relayList = relayList) + cache.justConsumeMyOwnEvent(signedEvent) - LocalCache.getNoteIfExists(signedEvent.id)?.let { onReady(it) } + cache.getNoteIfExists(signedEvent.id)?.let { onReady(it) } } fun createHeader( @@ -2104,7 +1114,7 @@ class Account( urlHeaderInfo: Map, caption: String?, contentWarningReason: String?, - relayList: List, + relayList: Set, onReady: (Note) -> Unit, ) { val iMetas = @@ -2148,7 +1158,7 @@ class Account( alt: String?, contentWarningReason: String?, originalHash: String? = null, - relayList: List, + relayList: Set, onReady: (Note) -> Unit, ) { if (!isWriteable()) return @@ -2221,9 +1231,79 @@ class Account( } } + fun signAndSendPrivately( + template: EventTemplate, + relayList: Set, + ) { + signer.sign(template) { + cache.justConsumeMyOwnEvent(it) + client.send(it, relayList) + } + } + + fun signAndSendPrivatelyOrBroadcast( + template: EventTemplate, + relayList: (T) -> List?, + onDone: (T) -> Unit = {}, + ) { + signer.sign(template) { + cache.justConsumeMyOwnEvent(it) + val relays = relayList(it) + if (relays != null && relays.isNotEmpty()) { + client.send(it, relays.toSet()) + } else { + client.send(it, computeRelayListToBroadcast(it)) + } + onDone(it) + } + } + + fun signAndSend( + template: EventTemplate, + relayList: Set, + broadcastNotes: Set, + ) { + signer.sign(template) { + cache.justConsumeMyOwnEvent(it) + client.send(it, relayList = relayList) + + broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relayList) } } + } + } + fun signAndSend( draftTag: String?, template: EventTemplate, + relayList: Set, + broadcastNotes: List, + ) = signAndSend(draftTag, template, relayList, mapEntitiesToNotes(broadcastNotes).toSet()) + + fun signAndSend( + draftTag: String?, + template: EventTemplate, + relayList: Set, + broadcastNotes: Set, + ) { + if (draftTag != null) { + signer.assembleRumor(template) { rumor -> + DraftEvent.create(draftTag, rumor, emptyList(), signer) { draftEvent -> + sendDraftEvent(draftEvent) + } + } + } else { + signer.sign(template) { + cache.justConsumeMyOwnEvent(it) + client.send(it, relayList = relayList) + + broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relayList) } } + } + } + } + + fun signAndSendNIP04Message( + draftTag: String?, + template: EventTemplate, + relayList: Set, ) { if (draftTag != null) { if (template.content.isEmpty()) { @@ -2237,79 +1317,8 @@ class Account( } } else { signer.sign(template) { - LocalCache.justConsumeMyOwnEvent(it) - Amethyst.instance.client.send(it) - } - } - } - - fun signAndSendPrivately( - template: EventTemplate, - relayList: List, - onDone: (T) -> Unit = {}, - ) { - signer.sign(template) { - LocalCache.justConsumeMyOwnEvent(it) - Amethyst.instance.client.sendPrivately(it, relayList = convertRelayList(relayList)) - onDone(it) - } - } - - fun signAndSendPrivatelyOrBroadcast( - template: EventTemplate, - relayList: (T) -> List?, - onDone: (T) -> Unit = {}, - ) { - signer.sign(template) { - LocalCache.justConsumeMyOwnEvent(it) - val relays = relayList(it) - if (relays != null) { - Amethyst.instance.client.sendPrivately(it, relayList = convertRelayList(relays)) - } else { - Amethyst.instance.client.send(it) - } - onDone(it) - } - } - - fun signAndSend( - template: EventTemplate, - relayList: List, - broadcastNotes: Set, - ) { - signer.sign(template) { - LocalCache.justConsumeMyOwnEvent(it) - Amethyst.instance.client.send(it, relayList = relayList) - - broadcastNotes.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } } - } - } - - fun signAndSend( - draftTag: String?, - template: EventTemplate, - relayList: List, - broadcastNotes: List, - ) = signAndSend(draftTag, template, relayList, mapEntitiesToNotes(broadcastNotes).toSet()) - - fun signAndSend( - draftTag: String?, - template: EventTemplate, - relayList: List, - broadcastNotes: Set, - ) { - if (draftTag != null) { - signer.assembleRumor(template) { rumor -> - DraftEvent.create(draftTag, rumor, emptyList(), signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - signer.sign(template) { - LocalCache.justConsumeMyOwnEvent(it) - Amethyst.instance.client.send(it, relayList = relayList) - - broadcastNotes.forEach { it.event?.let { Amethyst.instance.client.send(it, relayList = relayList) } } + cache.justConsumeMyOwnEvent(it) + client.send(it, relayList) } } } @@ -2317,7 +1326,7 @@ class Account( fun signAndSendWithList( draftTag: String?, template: EventTemplate, - relayList: List, + relayList: Collection, broadcastNotes: Set, ) { if (draftTag != null) { @@ -2328,21 +1337,12 @@ class Account( } } else { signer.sign(template) { - val connect = - relayList.map { - val normalizedUrl = RelayUrlFormatter.normalize(it) - RelaySetupInfoToConnect( - normalizedUrl, - shouldUseTorForClean(normalizedUrl), - true, - true, - setOf(FeedType.GLOBAL), - ) - } + cache.justConsumeMyOwnEvent(it) - LocalCache.justConsumeMyOwnEvent(it) - Amethyst.instance.client.sendPrivately(it, relayList = connect) - broadcastNotes.forEach { it.event?.let { Amethyst.instance.client.sendPrivately(it, relayList = connect) } } + val relaySet = relayList.toSet() + + client.send(it, relayList = relaySet) + broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relaySet) } } } } } @@ -2351,7 +1351,7 @@ class Account( draftTag: String?, template: EventTemplate, broadcastNotes: Set, - relayList: List, + relayList: Set, ) { if (!isWriteable()) return @@ -2370,23 +1370,12 @@ class Account( fun deleteDraft(draftTag: String) { val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag) - LocalCache.getAddressableNoteIfExists(key)?.let { note -> + cache.getAddressableNoteIfExists(key)?.let { note -> val noteEvent = note.event if (noteEvent is DraftEvent) { noteEvent.createDeletedEvent(signer) { - Amethyst.instance.client.sendPrivately( - it, - note.relays.map { it.url }.map { - RelaySetupInfoToConnect( - it, - shouldUseTorForClean(it), - false, - true, - emptySet(), - ) - }, - ) - LocalCache.justConsumeMyOwnEvent(it) + client.send(it, outboxRelays.flow.value + note.relays) + cache.justConsumeMyOwnEvent(it) } } delete(note) @@ -2395,9 +1384,9 @@ class Account( suspend fun createInteractiveStoryReadingState( root: InteractiveStoryBaseEvent, - rootRelay: String?, + rootRelay: NormalizedRelayUrl?, readingScene: InteractiveStoryBaseEvent, - readingSceneRelay: String?, + readingSceneRelay: NormalizedRelayUrl?, ) { if (!isWriteable()) return @@ -2409,15 +1398,13 @@ class Account( currentSceneRelay = readingSceneRelay, ) - signer.sign(template) { - sendToPrivateOutboxAndLocal(it) - } + signer.sign(template, ::sendToPrivateOutboxAndLocal) } suspend fun updateInteractiveStoryReadingState( readingState: InteractiveStoryReadingStateEvent, readingScene: InteractiveStoryBaseEvent, - readingSceneRelay: String?, + readingSceneRelay: NormalizedRelayUrl?, ) { if (!isWriteable()) return @@ -2428,9 +1415,7 @@ class Account( currentSceneRelay = readingSceneRelay, ) - signer.sign(template) { - sendToPrivateOutboxAndLocal(it) - } + signer.sign(template, ::sendToPrivateOutboxAndLocal) } fun mapEntitiesToNotes(entities: List): List = @@ -2438,10 +1423,10 @@ class Account( when (it) { is NPub -> null is NProfile -> null - is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> LocalCache.getOrCreateNote(it.hex) - is NEvent -> LocalCache.getOrCreateNote(it.hex) - is NEmbed -> LocalCache.getOrCreateNote(it.event.id) - is NAddress -> LocalCache.checkGetOrCreateAddressableNote(it.aTag()) + is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> cache.getOrCreateNote(it.hex) + is NEvent -> cache.getOrCreateNote(it.hex) + is NEmbed -> cache.getOrCreateNote(it.event.id) + is NAddress -> cache.checkGetOrCreateAddressableNote(it.aTag()) is NSec -> null is NRelay -> null else -> null @@ -2460,7 +1445,7 @@ class Account( zapRaiserAmount: Long? = null, imetas: List? = null, draftTag: String? = null, - relayList: List, + relayList: Set, ) { if (!isWriteable()) return @@ -2497,7 +1482,7 @@ class Account( zapRaiserAmount: Long? = null, imetas: List? = null, draftTag: String? = null, - relayList: List, + relayList: Set, ) { if (!isWriteable()) return @@ -2526,7 +1511,6 @@ class Account( value: BigDecimal, bounty: Note, draftTag: String?, - relayList: List, ) { if (!isWriteable()) return @@ -2540,7 +1524,9 @@ class Account( eventAuthor.toPTag(), ) - signAndSend(draftTag, template, relayList, setOf(bounty)) + val relays = bounty.relays + outboxRelays.flow.value + + signAndSendWithList(draftTag, template, relays, setOf(bounty)) } fun sendEdit( @@ -2548,7 +1534,7 @@ class Account( originalNote: Note, notify: HexKey?, summary: String? = null, - relayList: List, + relayList: List, ) { if (!isWriteable()) return @@ -2561,37 +1547,11 @@ class Account( summary = summary, signer = signer, ) { - LocalCache.justConsumeMyOwnEvent(it) - Amethyst.instance.client.send(it, relayList = relayList) + cache.justConsumeMyOwnEvent(it) + client.send(it, relayList = relayList.toSet()) } } - fun sendPrivateMessage( - message: String, - toUser: User, - replyingTo: Note? = null, - zapReceiver: List? = null, - contentWarningReason: String? = null, - zapRaiserAmount: Long? = null, - geohash: String? = null, - imetas: List? = null, - emojis: List? = null, - draftTag: String?, - ) { - sendPrivateMessage( - message, - toUser.toPTag(), - replyingTo, - zapReceiver, - contentWarningReason, - zapRaiserAmount, - geohash, - imetas, - emojis, - draftTag, - ) - } - fun sendPrivateMessage( message: String, toUser: PTag, @@ -2621,7 +1581,9 @@ class Account( contentWarningReason?.let { contentWarning(contentWarningReason) } } - signAndSend(draftTag, template) + val destinationRelays = cache.getOrCreateUser(toUser.pubKey).dmInboxRelays() + + signAndSendNIP04Message(draftTag, template, outboxRelays.flow.value + destinationRelays) } } @@ -2656,43 +1618,20 @@ class Account( } } - fun getPrivateOutBoxRelayList(): List = - normalizedPrivateOutBoxRelaySet.value.map { - RelaySetupInfoToConnect( - it, - shouldUseTorForClean(it), - true, - true, - emptySet(), - ) - } - fun sendDraftEvent(draftEvent: DraftEvent) { sendToPrivateOutboxAndLocal(draftEvent) } fun sendToPrivateOutboxAndLocal(event: Event) { - val relayList = normalizedPrivateOutBoxRelaySet.value + settings.localRelayServers + val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value if (relayList.isNotEmpty()) { - Amethyst.instance.client.sendPrivately(event, convertRelayList(relayList.toList())) + client.send(event, relayList.toSet()) } else { - Amethyst.instance.client.send(event) + client.send(event, outboxRelays.flow.value) } - LocalCache.justConsumeMyOwnEvent(event) + cache.justConsumeMyOwnEvent(event) } - fun convertRelayList(broadcast: List): List = - broadcast.map { - val normalizedUrl = RelayUrlFormatter.normalize(it) - RelaySetupInfoToConnect( - normalizedUrl, - shouldUseTorForClean(normalizedUrl), - true, - true, - setOf(FeedType.GLOBAL), - ) - } - fun broadcastPrivately(signedEvents: NIP17Factory.Result) { val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) } @@ -2700,140 +1639,72 @@ class Account( giftWrap.unwrap(signer) { gift -> if (gift is SealedRumorEvent) { gift.unseal(signer) { rumor -> - LocalCache.justConsumeMyOwnEvent(rumor) + cache.justConsumeMyOwnEvent(rumor) } } - LocalCache.justConsumeMyOwnEvent(gift) + cache.justConsumeMyOwnEvent(gift) } - LocalCache.justConsumeMyOwnEvent(giftWrap) + cache.justConsumeMyOwnEvent(giftWrap) } val id = mine.firstOrNull()?.id - val mineNote = if (id == null) null else LocalCache.getNoteIfExists(id) + val mineNote = if (id == null) null else cache.getNoteIfExists(id) signedEvents.wraps.forEach { wrap -> // Creates an alias if (mineNote != null && wrap.recipientPubKey() != signer.pubKey) { - LocalCache.getOrAddAliasNote(wrap.id, mineNote) + cache.getOrAddAliasNote(wrap.id, mineNote) } val receiver = wrap.recipientPubKey() if (receiver != null) { val relayList = ( - LocalCache + cache .getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver)) ?.event as? ChatMessageRelayListEvent - )?.relays()?.ifEmpty { null }?.map { - val normalizedUrl = RelayUrlFormatter.normalize(it) - RelaySetupInfoToConnect( - normalizedUrl, - shouldUseTorForClean(normalizedUrl), - false, - true, - feedTypes = setOf(FeedType.PRIVATE_DMS), - ) - } + )?.relays()?.ifEmpty { null }?.toSet() if (relayList != null) { - Amethyst.instance.client.sendPrivately(signedEvent = wrap, relayList = relayList) + client.send(signedEvent = wrap, relayList = relayList) } else { - Amethyst.instance.client.send(wrap) + val taggedUserInboxRelays = + wrap.taggedUserIds().flatMapTo(mutableSetOf()) { pubkey -> + if (pubkey == userProfile().pubkeyHex) { + notificationRelays.flow.value + } else { + LocalCache.getUserIfExists(pubkey)?.inboxRelays()?.ifEmpty { null }?.toSet() + ?: cache.relayHints.hintsForKey(pubkey).toSet() + } + } + + client.send(wrap, taggedUserInboxRelays) } } else { - Amethyst.instance.client.send(wrap) + client.send(wrap, outboxRelays.flow.value) } } } + fun createStatus(newStatus: String) = UserStatusAction.create(newStatus, signer, ::sendMyPublicAndPrivateOutbox) + fun updateStatus( oldStatus: AddressableNote, newStatus: String, - ) { - if (!isWriteable()) return - val oldEvent = oldStatus.event as? StatusEvent ?: return + ) = UserStatusAction.update(oldStatus, newStatus, signer, ::sendMyPublicAndPrivateOutbox) - StatusEvent.update(oldEvent, newStatus, signer) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + fun deleteStatus(oldStatus: AddressableNote) = UserStatusAction.delete(oldStatus, signer, ::sendMyPublicAndPrivateOutbox) - fun createStatus(newStatus: String) { - if (!isWriteable()) return + fun removeEmojiPack(emojiPack: Note) = emoji.removeEmojiPack(emojiPack, ::sendMyPublicAndPrivateOutbox) - StatusEvent.create(newStatus, "general", expiration = null, signer) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - - fun deleteStatus(oldStatus: AddressableNote) { - if (!isWriteable()) return - val oldEvent = oldStatus.event as? StatusEvent ?: return - - StatusEvent.clear(oldEvent, signer) { event -> - Amethyst.instance.client.send(event) - LocalCache.justConsumeMyOwnEvent(event) - - signer.sign( - DeletionEvent.buildForVersionOnly(listOf(event)), - ) { event2 -> - Amethyst.instance.client.send(event2) - LocalCache.justConsumeMyOwnEvent(event2) - } - } - } - - fun removeEmojiPack( - usersEmojiList: Note, - emojiPack: Note, - ) { - if (!isWriteable()) return - - val noteEvent = usersEmojiList.event - if (noteEvent !is EmojiPackSelectionEvent) return - val emojiPackEvent = emojiPack.event - if (emojiPackEvent !is EmojiPackEvent) return - - signer.sign(EmojiPackSelectionEvent.remove(noteEvent, emojiPackEvent)) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - - fun addEmojiPack( - usersEmojiList: Note, - emojiPack: Note, - ) { - if (!isWriteable()) return - val emojiPackEvent = emojiPack.event - if (emojiPackEvent !is EmojiPackEvent) return - - val eventHint = emojiPack.toEventHint() ?: return - - if (usersEmojiList.event == null) { - signer.sign(EmojiPackSelectionEvent.build(listOf(eventHint))) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - val noteEvent = usersEmojiList.event - if (noteEvent !is EmojiPackSelectionEvent) return - - signer.sign(EmojiPackSelectionEvent.add(noteEvent, eventHint)) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - } + fun addEmojiPack(emojiPack: Note) = emoji.addEmojiPack(emojiPack, ::sendMyPublicAndPrivateOutbox) fun addToGallery( idHex: HexKey, url: String, - relay: String?, + relay: NormalizedRelayUrl?, blurhash: String?, dim: DimensionTag?, hash: String?, @@ -2850,8 +1721,8 @@ class Account( blurhash?.let { blurhash(it) } }, ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) + client.send(it, outboxRelays.flow.value) + cache.justConsumeMyOwnEvent(it) } } @@ -2873,8 +1744,8 @@ class Account( isPrivate, signer, ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) + client.send(it, outboxRelays.flow.value) + cache.justConsumeMyOwnEvent(it) } } else { BookmarkListEvent.addEvent( @@ -2883,8 +1754,8 @@ class Account( isPrivate, signer, ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) + client.send(it, outboxRelays.flow.value) + cache.justConsumeMyOwnEvent(it) } } } @@ -2904,8 +1775,8 @@ class Account( isPrivate, signer, ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) + client.send(it, outboxRelays.flow.value) + cache.justConsumeMyOwnEvent(it) } } else { BookmarkListEvent.removeEvent( @@ -2914,23 +1785,23 @@ class Account( isPrivate, signer, ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) + client.send(it, outboxRelays.flow.value) + cache.justConsumeMyOwnEvent(it) } } } fun sendAuthEvent( - relay: Relay, + relay: IRelayClient, challenge: String, ) { createAuthEvent(relay.url, challenge) { - Amethyst.instance.client.sendIfExists(it, relay) + client.sendIfExists(it, relay.url) } } fun createAuthEvent( - relayUrl: String, + relayUrl: NormalizedRelayUrl, challenge: String, onReady: (RelayAuthEvent) -> Unit, ) { @@ -2940,7 +1811,7 @@ class Account( } fun createAuthEvent( - relayUrls: List, + relayUrls: List, challenge: String, onReady: (RelayAuthEvent) -> Unit, ) { @@ -2976,165 +1847,53 @@ class Account( fun isInPublicBookmarks(note: Note): Boolean { if (!isWriteable()) return false - if (note is AddressableNote) { - return userProfile().latestBookmarkList?.isTaggedAddressableNote(note.idHex) == true + return if (note is AddressableNote) { + userProfile().latestBookmarkList?.isTaggedAddressableNote(note.idHex) == true } else { - return userProfile().latestBookmarkList?.isTaggedEvent(note.idHex) == true + userProfile().latestBookmarkList?.isTaggedEvent(note.idHex) == true } } - fun getAppSpecificDataNote() = LocalCache.getOrCreateAddressableNote(AppSpecificDataEvent.createAddress(userProfile().pubkeyHex, APP_SPECIFIC_DATA_D_TAG)) - - fun getAppSpecificDataFlow(): StateFlow = getAppSpecificDataNote().flow().metadata.stateFlow - - fun getBlockListNote() = LocalCache.getOrCreateAddressableNote(PeopleListEvent.createBlockAddress(userProfile().pubkeyHex)) - - fun getMuteListNote() = LocalCache.getOrCreateAddressableNote(MuteListEvent.createAddress(userProfile().pubkeyHex)) - - fun getMuteListFlow(): StateFlow = getMuteListNote().flow().metadata.stateFlow - - fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent - - fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent - fun hideWord(word: String) { - val muteList = getMuteList() + if (!isWriteable()) return - if (muteList != null) { - MuteListEvent.addWord( - earlierVersion = muteList, - word = word, - isPrivate = true, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - MuteListEvent.createListWithWord( - word = word, - isPrivate = true, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + muteList.hideWord(word, ::sendMyPublicAndPrivateOutbox) } fun showWord(word: String) { - val blockList = getBlockList() + if (!isWriteable()) return - if (blockList != null) { - PeopleListEvent.removeWord( - earlierVersion = blockList, - word = word, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - - val muteList = getMuteList() - - if (muteList != null) { - MuteListEvent.removeWord( - earlierVersion = muteList, - word = word, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + blockPeopleList.showWord(word, ::sendMyPublicAndPrivateOutbox) + muteList.showWord(word, ::sendMyPublicAndPrivateOutbox) } - fun hideUser(pubkeyHex: String) { - val muteList = getMuteList() + fun hideUser(pubkeyHex: HexKey) { + if (!isWriteable()) return - if (muteList != null) { - MuteListEvent.addUser( - earlierVersion = muteList, - pubKeyHex = pubkeyHex, - isPrivate = true, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - MuteListEvent.createListWithUser( - pubKeyHex = pubkeyHex, - isPrivate = true, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + muteList.hideUser(pubkeyHex, ::sendMyPublicAndPrivateOutbox) } - fun showUser(pubkeyHex: String) { - val blockList = getBlockList() + fun showUser(pubkeyHex: HexKey) { + if (!isWriteable()) return - if (blockList != null) { - PeopleListEvent.removeUser( - earlierVersion = blockList, - pubKeyHex = pubkeyHex, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - - val muteList = getMuteList() - - if (muteList != null) { - MuteListEvent.removeUser( - earlierVersion = muteList, - pubKeyHex = pubkeyHex, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - - transientHiddenUsers.update { - it - pubkeyHex - } + blockPeopleList.showUser(pubkeyHex, ::sendMyPublicAndPrivateOutbox) + muteList.showUser(pubkeyHex, ::sendMyPublicAndPrivateOutbox) + hiddenUsers.showUser(pubkeyHex) } fun requestDVMContentDiscovery( - dvmPublicKey: String, + dvmPublicKey: User, onReady: (event: NIP90ContentDiscoveryRequestEvent) -> Unit, ) { - NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey, signer.pubKey, getReceivingRelays(), signer) { + val relays = nip65RelayList.inboxFlow.value.toSet() + NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey.pubkeyHex, signer.pubKey, relays, signer) { val relayList = - ( - LocalCache - .getAddressableNoteIfExists( - AdvertisedRelayListEvent.createAddressTag(dvmPublicKey), - )?.event as? AdvertisedRelayListEvent - )?.readRelays()?.ifEmpty { null }?.map { - val normalizedUrl = RelayUrlFormatter.normalize(it) - RelaySetupInfoToConnect( - normalizedUrl, - shouldUseTorForClean(normalizedUrl), - true, - true, - setOf(FeedType.GLOBAL), - ) - } + dvmPublicKey.inboxRelays().ifEmpty { + LocalCache.relayHints.hintsForKey(dvmPublicKey.pubkeyHex) + }.toSet() - if (relayList != null) { - Amethyst.instance.client.sendPrivately(it, relayList) - } else { - Amethyst.instance.client.send(it) - } - LocalCache.justConsumeMyOwnEvent(it) + client.send(it, relayList) + cache.justConsumeMyOwnEvent(it) onReady(it) } } @@ -3211,59 +1970,13 @@ class Account( } } - // Takes a User's relay list and adds the types of feeds they are active for. - fun kind3Relays(): Array? { - val usersRelayList = - (userProfile().latestContactList ?: settings.backupContactList) - ?.relays() - ?.map { - val url = RelayUrlFormatter.normalize(it.key) - - val localFeedTypes = - settings.localRelays - .firstOrNull { localRelay -> RelayUrlFormatter.normalize(localRelay.url) == url } - ?.feedTypes - ?.minus(setOf(FeedType.SEARCH, FeedType.WALLET_CONNECT)) - ?: Constants.defaultRelays - .filter { defaultRelay -> RelayUrlFormatter.normalize(defaultRelay.url) == url } - .firstOrNull() - ?.feedTypes - ?: Constants.activeTypesGlobalChats - - RelaySetupInfo(url, it.value.read, it.value.write, localFeedTypes) - }?.ifEmpty { null } ?: return null - - return usersRelayList.toTypedArray() - } - - fun convertLocalRelays(): Array = - settings.localRelays - .map { - RelaySetupInfo( - RelayUrlFormatter.normalize(it.url), - it.read, - it.write, - it.feedTypes.minus(setOf(FeedType.SEARCH, FeedType.WALLET_CONNECT)), - ) - }.toTypedArray() - - fun activeGlobalRelays(): Array = - connectToRelays.value - .filter { it.feedTypes.contains(FeedType.GLOBAL) } - .map { it.url } - .toTypedArray() - - fun activeWriteRelays(): List = connectToRelays.value.filter { it.write } - fun isAllHidden(users: Set): Boolean = users.all { isHidden(it) } fun isHidden(user: User) = isHidden(user.pubkeyHex) - fun isHidden(userHex: String): Boolean = - flowHiddenUsers.value.hiddenUsers.contains(userHex) || - flowHiddenUsers.value.spammers.contains(userHex) + fun isHidden(userHex: String): Boolean = hiddenUsers.flow.value.isUserHidden(userHex) - fun followingKeySet(): Set = liveKind3Follows.value.authors + fun followingKeySet(): Set = kind3FollowList.flow.value.authors fun isAcceptable(user: User): Boolean { if (userProfile().pubkeyHex == user.pubkeyHex) { @@ -3321,229 +2034,46 @@ class Account( } return ( - note.reportsBy(liveKind3Follows.value.authorsPlusMe) + - (note.author?.reportsBy(liveKind3Follows.value.authorsPlusMe) ?: emptyList()) + + note.reportsBy(kind3FollowList.flow.value.authorsPlusMe) + + (note.author?.reportsBy(kind3FollowList.flow.value.authorsPlusMe) ?: emptyList()) + innerReports ).toSet() } - fun saveKind3RelayList(value: List) { - settings.updateLocalRelays(value.toSet()) - sendKind3RelayList( - value.associate { it.url to ReadWrite(it.read, it.write) }, - ) - } - - fun getDMRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(ChatMessageRelayListEvent.createAddress(signer.pubKey)) - - fun getDMRelayListFlow(): StateFlow = getDMRelayListNote().flow().metadata.stateFlow - - fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent - - fun saveDMRelayList(dmRelays: List) { + fun saveDMRelayList(dmRelays: List) { if (!isWriteable()) return - - val relayListForDMs = getDMRelayList() - if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) { - ChatMessageRelayListEvent.updateRelayList( - earlierVersion = relayListForDMs, - relays = dmRelays, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - ChatMessageRelayListEvent.createFromScratch( - relays = dmRelays, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + dmRelayList.saveRelayList(dmRelays, ::sendMyPublicAndPrivateOutbox) } - fun getPrivateOutboxRelayListNote(): AddressableNote = - LocalCache.getOrCreateAddressableNote( - PrivateOutboxRelayListEvent.createAddress(signer.pubKey), - ) - - fun getPrivateOutboxRelayListFlow(): StateFlow = getPrivateOutboxRelayListNote().flow().metadata.stateFlow - - fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent - - fun savePrivateOutboxRelayList(relays: List) { + fun savePrivateOutboxRelayList(relays: List) { if (!isWriteable()) return - - val relayListForPrivateOutbox = getPrivateOutboxRelayList() - - if (relayListForPrivateOutbox != null && !relayListForPrivateOutbox.cachedPrivateTags().isNullOrEmpty()) { - PrivateOutboxRelayListEvent.updateRelayList( - earlierVersion = relayListForPrivateOutbox, - relays = relays, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - PrivateOutboxRelayListEvent.createFromScratch( - relays = relays, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + privateStorageRelayList.saveRelayList(relays, ::sendMyPublicAndPrivateOutbox) } - fun getSearchRelayListNote(): AddressableNote = - LocalCache.getOrCreateAddressableNote( - SearchRelayListEvent.createAddress(signer.pubKey), - ) - - fun getSearchRelayListFlow(): StateFlow = getSearchRelayListNote().flow().metadata.stateFlow - - fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent - - fun saveSearchRelayList(searchRelays: List) { + fun saveSearchRelayList(searchRelays: List) { if (!isWriteable()) return - - val relayListForSearch = getSearchRelayList() - - if (relayListForSearch != null && relayListForSearch.tags.isNotEmpty()) { - SearchRelayListEvent.updateRelayList( - earlierVersion = relayListForSearch, - relays = searchRelays, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - SearchRelayListEvent.createFromScratch( - relays = searchRelays, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + searchRelayList.saveRelayList(searchRelays, ::sendMyPublicAndPrivateOutbox) } - fun getNIP65RelayListNote(pubkey: HexKey = signer.pubKey): AddressableNote = - LocalCache.getOrCreateAddressableNote( - AdvertisedRelayListEvent.createAddress(pubkey), - ) - - fun getNIP65RelayListFlow(pubkey: HexKey = signer.pubKey): StateFlow = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow - - fun getNIP65RelayList(pubkey: HexKey = signer.pubKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent - - fun sendNip65RelayList(relays: List) { + fun sendNip65RelayList(relays: List) { if (!isWriteable()) return - - val nip65RelayList = getNIP65RelayList() - - if (nip65RelayList != null) { - AdvertisedRelayListEvent.updateRelayList( - earlierVersion = nip65RelayList, - relays = relays, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - AdvertisedRelayListEvent.createFromScratch( - relays = relays, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } - } - - fun getFileServersList(): FileServersEvent? = getFileServersNote().event as? FileServersEvent - - fun getFileServersListFlow(): StateFlow = getFileServersNote().flow().metadata.stateFlow - - fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(FileServersEvent.createAddress(userProfile().pubkeyHex)) - - fun getBlossomServersList(): BlossomServersEvent? = getBlossomServersNote().event as? BlossomServersEvent - - fun getBlossomServersListFlow(): StateFlow = getBlossomServersNote().flow().metadata.stateFlow - - fun getBlossomServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(BlossomServersEvent.createAddress(userProfile().pubkeyHex)) - - fun host(url: String): String = - try { - URIReference.parse(url).host.value - } catch (e: Exception) { - url - } - - fun mergeServerList( - nip96: FileServersEvent?, - blossom: BlossomServersEvent?, - ): List { - val nip96servers = nip96?.servers()?.map { ServerName(host(it), it, ServerType.NIP96) } ?: emptyList() - val blossomServers = blossom?.servers()?.map { ServerName(host(it), it, ServerType.Blossom) } ?: emptyList() - - val result = (nip96servers + blossomServers).ifEmpty { DEFAULT_MEDIA_SERVERS } - - return result + ServerName("NIP95", "", ServerType.NIP95) + nip65RelayList.saveRelayList(relays, ::sendMyPublicAndPrivateOutbox) } fun sendFileServersList(servers: List) { if (!isWriteable()) return - - val serverList = getFileServersList() - - val template = - if (serverList != null && serverList.tags.isNotEmpty()) { - FileServersEvent.replaceServers(serverList, servers) - } else { - FileServersEvent.build(servers) - } - - signer.sign(template) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } + fileStorageServers.saveFileServersList(servers, ::sendMyPublicAndPrivateOutbox) } fun sendBlossomServersList(servers: List) { if (!isWriteable()) return - - val serverList = getBlossomServersList() - - if (serverList != null && serverList.tags.isNotEmpty()) { - BlossomServersEvent.updateRelayList( - earlierVersion = serverList, - relays = servers, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } else { - BlossomServersEvent.createFromScratch( - relays = servers, - signer = signer, - ) { - Amethyst.instance.client.send(it) - LocalCache.justConsumeMyOwnEvent(it) - } - } + blossomServers.saveBlossomServersList(servers, ::sendMyPublicAndPrivateOutbox) } fun getAllPeopleLists(): List = getAllPeopleLists(signer.pubKey) fun getAllPeopleLists(pubkey: HexKey): List = - LocalCache.addressables + cache.addressables .filter { _, addressableNote -> val noteEvent = addressableNote.event @@ -3596,9 +2126,9 @@ class Account( normalizedUrl: String, final: Boolean, ): Boolean = - if (isLocalHost(normalizedUrl)) { + if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) { false - } else if (isOnionUrl(normalizedUrl)) { + } else if (RelayUrlNormalizer.isOnion(normalizedUrl)) { true } else { final @@ -3632,46 +2162,26 @@ class Account( TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value } - fun shouldUseTorForDirty(dirtyUrl: String) = shouldUseTorForClean(RelayUrlFormatter.normalize(dirtyUrl)) - - fun shouldUseTorForClean(normalizedUrl: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> shouldUseTor(normalizedUrl) - TorType.EXTERNAL -> shouldUseTor(normalizedUrl) - } + fun shouldUseTorForClean(relay: NormalizedRelayUrl) = torRelayState.flow.value.useTor(relay) private fun checkLocalHostOnionAndThen( - normalizedUrl: String, + url: String, final: Boolean, - ): Boolean = checkLocalHostOnionAndThen(normalizedUrl, settings.torSettings.onionRelaysViaTor.value, final) + ): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final) private fun checkLocalHostOnionAndThen( normalizedUrl: String, isOnionRelaysActive: Boolean, final: Boolean, ): Boolean = - if (isLocalHost(normalizedUrl)) { + if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) { false - } else if (isOnionUrl(normalizedUrl)) { + } else if (RelayUrlNormalizer.isOnion(normalizedUrl)) { isOnionRelaysActive } else { final } - private fun shouldUseTor(normalizedUrl: String): Boolean = - if (isLocalHost(normalizedUrl)) { - false - } else if (isOnionUrl(normalizedUrl)) { - settings.torSettings.onionRelaysViaTor.value - } else if (isDMRelay(normalizedUrl)) { - settings.torSettings.dmRelaysViaTor.value - } else if (isTrustedRelay(normalizedUrl)) { - settings.torSettings.trustedRelaysViaTor.value - } else { - settings.torSettings.newRelaysViaTor.value - } - fun shouldUseTorForMoneyOperations(url: String) = when (settings.torSettings.torType.value) { TorType.OFF -> false @@ -3693,14 +2203,6 @@ class Account( TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) } - fun isLocalHost(url: String) = url.contains("//127.0.0.1") || url.contains("//localhost") - - fun isOnionUrl(url: String) = url.contains(".onion") - - fun isDMRelay(url: String) = url in normalizedDmRelaySet.value - - fun isTrustedRelay(url: String): Boolean = connectToRelays.value.any { it.url == url } || url == settings.zapPaymentRequest?.relayUri - fun otsResolver(): OtsResolver = OtsResolverBuilder().build( Amethyst.instance.okHttpClients, @@ -3710,215 +2212,13 @@ class Account( init { Log.d("AccountRegisterObservers", "Init") - settings.backupContactList?.let { - Log.d("AccountRegisterObservers", "Loading saved contacts ${it.toJson()}") - - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } - } - - settings.backupUserMetadata?.let { - Log.d("AccountRegisterObservers", "Loading saved user metadata ${it.toJson()}") - - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } - } - - settings.backupDMRelayList?.let { - Log.d("AccountRegisterObservers", "Loading saved DM Relay List ${it.toJson()}") - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } - } - - settings.backupNIP65RelayList?.let { - Log.d("AccountRegisterObservers", "Loading saved nip65 relay list ${it.toJson()}") - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } - } - - settings.backupSearchRelayList?.let { - Log.d("AccountRegisterObservers", "Loading saved search relay list ${it.toJson()}") - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } - } - - settings.backupPrivateHomeRelayList?.let { event -> - Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}") - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } - } - } - - settings.backupAppSpecificData?.let { event -> - Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}") - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { - LocalCache.justConsumeMyOwnEvent(event) - signer.decrypt(event.content, event.pubKey) { decrypted -> - try { - val syncedSettings = EventMapper.mapper.readValue(decrypted) - settings.syncedSettings.updateFrom(syncedSettings) - } catch (e: Throwable) { - if (e is CancellationException) throw e - Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) - e.printStackTrace() - AccountSyncedSettingsInternal() - } - } - } - } - - settings.backupMuteList?.let { event -> - Log.d("AccountRegisterObservers", "Loading saved mute list ${event.toJson()}") - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } - } - } - - settings.backupEphemeralChatList?.let { event -> - Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list ${event.toJson()}") - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } - } - } - - settings.backupChannelList?.let { event -> - Log.d("AccountRegisterObservers", "Loading saved channel list ${event.toJson()}") - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } - } - } - - // saves contact list for the next time. - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "Kind 0 Collector Start") - userProfile().flow().metadata.stateFlow.collect { - Log.d("AccountRegisterObservers", "Updating Kind 0 ${userProfile().toBestDisplayName()}") - settings.updateUserMetadata(userProfile().latestMetadata) - } - } - - // saves contact list for the next time. - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "Kind 3 Collector Start") - userProfile().flow().follows.stateFlow.collect { - Log.d("AccountRegisterObservers", "Updating Kind 3 ${userProfile().toBestDisplayName()}") - settings.updateContactListTo(userProfile().latestContactList) - } - } scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start") - getDMRelayListFlow().collect { - Log.d("AccountRegisterObservers", "Updating DM Relay List for ${userProfile().toBestDisplayName()}") - (it.note.event as? ChatMessageRelayListEvent)?.let { - settings.updateDMRelayList(it) - } - } - } - - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "NIP-65 Relay List Collector Start") - getNIP65RelayListFlow().collect { - Log.d("AccountRegisterObservers", "Updating NIP-65 List for ${userProfile().toBestDisplayName()}") - (it.note.event as? AdvertisedRelayListEvent)?.let { - settings.updateNIP65RelayList(it) - } - } - } - - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "Search Relay List Collector Start") - getSearchRelayListFlow().collect { - Log.d("AccountRegisterObservers", "Updating Search Relay List for ${userProfile().toBestDisplayName()}") - (it.note.event as? SearchRelayListEvent)?.let { - settings.updateSearchRelayList(it) - } - } - } - - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "Private Home Relay List Collector Start") - getPrivateOutboxRelayListFlow().collect { - Log.d("AccountRegisterObservers", "Updating Private Home Relay List for ${userProfile().toBestDisplayName()}") - (it.note.event as? PrivateOutboxRelayListEvent)?.let { - settings.updatePrivateHomeRelayList(it) - } - } - } - - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "Mute List Collector Start") - getMuteListFlow().collect { - Log.d("AccountRegisterObservers", "Updating Mute List for ${userProfile().toBestDisplayName()}") - (it.note.event as? MuteListEvent)?.let { - settings.updateMuteList(it) - } - } - } - - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "Channel List Collector Start") - publicChatList.getChannelListFlow().collect { - Log.d("AccountRegisterObservers", "Channel List for ${userProfile().toBestDisplayName()}") - (it.note.event as? ChannelListEvent)?.let { - settings.updateChannelListTo(it) - } - } - } - - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "EphemeralChatList Collector Start") - ephemeralChatList.getEphemeralChatListFlow().collect { - Log.d("AccountRegisterObservers", "EphemeralChatList List for ${userProfile().toBestDisplayName()}") - (it.note.event as? EphemeralChatListEvent)?.let { - settings.updateEphemeralChatListTo(it) - } - } - } - - scope.launch(Dispatchers.Default) { - Log.d("AccountRegisterObservers", "AppSpecificData Collector Start") - getAppSpecificDataFlow().collect { - Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${userProfile().toBestDisplayName()}") - (it.note.event as? AppSpecificDataEvent)?.let { - signer.decrypt(it.content, it.pubKey) { decrypted -> - val syncedSettings = - try { - EventMapper.mapper.readValue(decrypted) - } catch (e: Throwable) { - if (e is CancellationException) throw e - Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) - e.printStackTrace() - AccountSyncedSettingsInternal() - } - - settings.updateAppSpecificData(it, syncedSettings) - } - } - } - } - - scope.launch(Dispatchers.Default) { - LocalCache.antiSpam.flowSpam.collect { + cache.antiSpam.flowSpam.collect { it.cache.spamMessages.snapshot().values.forEach { spammer -> - if (spammer.pubkeyHex !in transientHiddenUsers.value && spammer.duplicatedMessages.size >= 5) { + if (!hiddenUsers.isHidden(spammer.pubkeyHex) && spammer.duplicatedMessages.size >= 5) { if (spammer.pubkeyHex != userProfile().pubkeyHex && spammer.pubkeyHex !in followingKeySet()) { - transientHiddenUsers.update { - it + spammer.pubkeyHex - } + hiddenUsers.hideUser(spammer.pubkeyHex) } } } @@ -3929,11 +2229,11 @@ class Account( delay(1000 * 60 * 1) // waits 5 minutes before migrating the list. val contactList = userProfile().latestContactList - val oldChannels = contactList?.taggedEventIds()?.toSet()?.mapNotNull { LocalCache.getChannelIfExists(it) as? PublicChatChannel } + val oldChannels = contactList?.taggedEventIds()?.toSet()?.mapNotNull { cache.getChannelIfExists(it) as? PublicChatChannel } if (oldChannels != null && oldChannels.isNotEmpty()) { Log.d("DB UPGRADE", "Migrating List with ${oldChannels.size} old channels ") - val existingChannels = publicChatList.livePublicChatEventIdSet.value + val existingChannels = publicChatList.flowSet.value val needsToUpgrade = oldChannels.filter { it.idHex !in existingChannels } @@ -3941,10 +2241,55 @@ class Account( if (needsToUpgrade.isNotEmpty()) { Log.d("DB UPGRADE", "Migrating List") - publicChatList.follow(oldChannels) { - sendToPrivateOutboxAndLocal(it) - LocalCache.justConsumeMyOwnEvent(it) - } + publicChatList.follow(oldChannels, ::sendMyPublicAndPrivateOutbox) + } + } + + val oldCommunities = contactList?.taggedAddresses()?.toSet()?.map { cache.getOrCreateAddressableNote(it) } + + if (oldCommunities != null && oldCommunities.isNotEmpty()) { + Log.d("DB UPGRADE", "Migrating List with ${oldCommunities.size} old communities ") + val existingCommunities = communityList.flowSet.value + + val needsToUpgrade = oldCommunities.filter { it.idHex !in existingCommunities } + + Log.d("DB UPGRADE", "Migrating List with ${needsToUpgrade.size} needsToUpgrade ") + + if (needsToUpgrade.isNotEmpty()) { + Log.d("DB UPGRADE", "Migrating List") + communityList.follow(oldCommunities, ::sendMyPublicAndPrivateOutbox) + } + } + + val oldHashtags = contactList?.hashtags()?.toSet() + + if (oldHashtags != null && oldHashtags.isNotEmpty()) { + Log.d("DB UPGRADE", "Migrating List with ${oldHashtags.size} old communities ") + val existingHashtags = hashtagList.flow.value + + val needsToUpgrade = oldHashtags.filter { it !in existingHashtags } + + Log.d("DB UPGRADE", "Migrating List with ${needsToUpgrade.size} needsToUpgrade ") + + if (needsToUpgrade.isNotEmpty()) { + Log.d("DB UPGRADE", "Migrating List") + hashtagList.follow(oldHashtags.toList(), ::sendMyPublicAndPrivateOutbox) + } + } + + val oldGeohashes = contactList?.geohashes()?.toSet() + + if (oldGeohashes != null && oldGeohashes.isNotEmpty()) { + Log.d("DB UPGRADE", "Migrating List with ${oldGeohashes.size} old communities ") + val existingGeohashes = geohashList.flow.value + + val needsToUpgrade = oldGeohashes.filter { it !in existingGeohashes } + + Log.d("DB UPGRADE", "Migrating List with ${needsToUpgrade.size} needsToUpgrade ") + + if (needsToUpgrade.isNotEmpty()) { + Log.d("DB UPGRADE", "Migrating List") + geohashList.follow(oldGeohashes.toList(), ::sendMyPublicAndPrivateOutbox) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 372df358d4..2933b790f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -27,8 +27,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow -import com.vitorpamplona.ammolite.relays.Constants -import com.vitorpamplona.ammolite.relays.RelaySetupInfo import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -43,11 +41,15 @@ import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncher import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter +import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo +import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType +import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow @@ -67,38 +69,29 @@ val DefaultChannelSet = val DefaultChannels = listOf( // Anigma's Nostr - EventIdHint("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", "wss://nos.lol"), + EventIdHint("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", Constants.nos), // Amethyst's Group - EventIdHint("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", "wss://nos.lol"), + EventIdHint("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", Constants.nos), ) +val DefaultNIP65RelaySet = setOf(Constants.mom, Constants.nos, Constants.bitcoiner) + val DefaultNIP65List = listOf( - AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.mom/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH), - AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nos.lol/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH), - AdvertisedRelayListEvent.AdvertisedRelayInfo(RelayUrlFormatter.normalize("wss://nostr.bitcoiner.social/"), AdvertisedRelayListEvent.AdvertisedRelayType.BOTH), + AdvertisedRelayInfo(Constants.mom, AdvertisedRelayType.BOTH), + AdvertisedRelayInfo(Constants.nos, AdvertisedRelayType.BOTH), + AdvertisedRelayInfo(Constants.bitcoiner, AdvertisedRelayType.BOTH), ) -val DefaultDMRelayList = - listOf( - RelayUrlFormatter.normalize("wss://auth.nostr1.com"), - RelayUrlFormatter.normalize("wss://relay.0xchat.com"), - RelayUrlFormatter.normalize("wss://nos.lol"), - ) +val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos) -val DefaultSearchRelayList = - listOf( - RelayUrlFormatter.normalize("wss://relay.nostr.band"), - RelayUrlFormatter.normalize("wss://nostr.wine"), - RelayUrlFormatter.normalize("wss://relay.noswhere.com"), - RelayUrlFormatter.normalize("wss://search.nos.today"), - ) +val DefaultSearchRelayList = setOf(Constants.band, Constants.wine, Constants.where, Constants.nostoday) // This has spaces to avoid mixing with a potential NIP-51 list with the same name. val GLOBAL_FOLLOWS = " Global " // This has spaces to avoid mixing with a potential NIP-51 list with the same name. -val KIND3_FOLLOWS = " All Follows " +val ALL_FOLLOWS = " All Follows " // This has spaces to avoid mixing with a potential NIP-51 list with the same name. val AROUND_ME = " Around Me " @@ -108,14 +101,13 @@ class AccountSettings( val keyPair: KeyPair, val transientAccount: Boolean = false, var externalSignerPackageName: String? = null, - var localRelays: Set = Constants.defaultRelays.toSet(), - var localRelayServers: Set = setOf(), + var localRelayServers: MutableStateFlow> = MutableStateFlow(setOf()), var defaultFileServer: ServerName = DEFAULT_MEDIA_SERVERS[0], - val defaultHomeFollowList: MutableStateFlow = MutableStateFlow(KIND3_FOLLOWS), + val defaultHomeFollowList: MutableStateFlow = MutableStateFlow(ALL_FOLLOWS), val defaultStoriesFollowList: MutableStateFlow = MutableStateFlow(GLOBAL_FOLLOWS), val defaultNotificationFollowList: MutableStateFlow = MutableStateFlow(GLOBAL_FOLLOWS), val defaultDiscoveryFollowList: MutableStateFlow = MutableStateFlow(GLOBAL_FOLLOWS), - var zapPaymentRequest: Nip47WalletConnect.Nip47URI? = null, + var zapPaymentRequest: Nip47WalletConnect.Nip47URINorm? = null, var hideDeleteRequestDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false, var hideNIP17WarningDialog: Boolean = false, @@ -128,6 +120,9 @@ class AccountSettings( var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null, var backupAppSpecificData: AppSpecificDataEvent? = null, var backupChannelList: ChannelListEvent? = null, + var backupCommunityList: CommunityListEvent? = null, + var backupHashtagList: HashtagListEvent? = null, + var backupGeohashList: GeohashListEvent? = null, var backupEphemeralChatList: EphemeralChatListEvent? = null, val torSettings: TorSettingsFlow = TorSettingsFlow(), val lastReadPerRoute: MutableStateFlow>> = MutableStateFlow(mapOf()), @@ -200,7 +195,7 @@ class AccountSettings( return false } - fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URI?): Boolean { + fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URINorm?): Boolean { if (zapPaymentRequest != newServer) { zapPaymentRequest = newServer saveAccountSettings() @@ -256,11 +251,11 @@ class AccountSettings( // proxy settings // --- fun setTorSettings(newTorSettings: TorSettings): Boolean { - if (torSettings.update(newTorSettings)) { + return if (torSettings.update(newTorSettings)) { saveAccountSettings() - return true + true } else { - return false + false } } @@ -301,8 +296,8 @@ class AccountSettings( // ---- fun updateLocalRelayServers(servers: Set) { - if (localRelayServers != servers) { - localRelayServers = servers + if (localRelayServers.value != servers) { + localRelayServers.update { servers } saveAccountSettings() } } @@ -377,6 +372,36 @@ class AccountSettings( } } + fun updateGeohashListTo(newGeohashList: GeohashListEvent?) { + if (newGeohashList == null || newGeohashList.tags.isEmpty()) return + + // Events might be different objects, we have to compare their ids. + if (backupGeohashList?.id != newGeohashList.id) { + backupGeohashList = newGeohashList + saveAccountSettings() + } + } + + fun updateHashtagListTo(newHashtagList: HashtagListEvent?) { + if (newHashtagList == null || newHashtagList.tags.isEmpty()) return + + // Events might be different objects, we have to compare their ids. + if (backupHashtagList?.id != newHashtagList.id) { + backupHashtagList = newHashtagList + saveAccountSettings() + } + } + + fun updateCommunityListTo(newCommunityList: CommunityListEvent?) { + if (newCommunityList == null || newCommunityList.tags.isEmpty()) return + + // Events might be different objects, we have to compare their ids. + if (backupCommunityList?.id != newCommunityList.id) { + backupCommunityList = newCommunityList + saveAccountSettings() + } + } + fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) { if (newEphemeralChatList == null || newEphemeralChatList.tags.isEmpty()) return @@ -492,17 +517,6 @@ class AccountSettings( } } - // ---- - // local relays - // ---- - - fun updateLocalRelays(newLocalRelays: Set) { - if (!localRelays.equals(newLocalRelays)) { - localRelays = newLocalRelays - saveAccountSettings() - } - } - // --- // attestations // --- diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index ed7d2b66df..c1c433c1c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -24,10 +24,10 @@ import android.util.Log import android.util.LruCache import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.njumpLink -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.ammolite.relays.RelayStats import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import kotlinx.coroutines.flow.MutableStateFlow @@ -45,7 +45,7 @@ class AntiSpamFilter { fun isSpam( event: Event, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, ): Boolean { checkNotInMainThread() @@ -74,14 +74,14 @@ class AntiSpamFilter { ) { Log.w( "Potential SPAM Message", - "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}", + "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} $relay ${event.content.replace("\n", " | ")}", ) // Log down offenders logOffender(hash, event) if (relay != null) { - RelayStats.newSpam(relay.url, njumpLink(NEvent.create(event.id, event.pubKey, event.kind, relay.url))) + RelayStats.newSpam(relay, njumpLink(NEvent.create(event.id, event.pubKey, event.kind, relay))) } flowSpam.tryEmit(AntiSpamState(this)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index fd604bfbe1..b7ba6f7c7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.note.toShortenHex -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList @@ -37,7 +37,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.toNEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData +import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.LargeCache @@ -51,7 +51,7 @@ class EphemeralChatChannel( override fun idDisplayNote() = idNote().toShortenHex() - override fun relays() = listOf(roomId.relayUrl) + override fun relays() = setOf(roomId.relayUrl) override fun toBestDisplayName() = roomId.toDisplayKey() @@ -68,17 +68,21 @@ class PublicChatChannel( ) : Channel(idHex) { var event: ChannelCreateEvent? = null var infoTags = EmptyTagList - var info = ChannelData(null, null, null, null) + var info = ChannelDataNorm(null, null, null, null) - override fun relays() = info.relays ?: super.relays() + override fun relays() = info.relays?.toSet() ?: super.relays() - fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, *relays().toTypedArray()) + fun relayHintUrls() = relays().take(3) + + fun relayHintUrl() = relays().firstOrNull() + + fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, relayHintUrls()) fun toNostrUri() = "nostr:${toNEvent()}" - fun toEventHint() = event?.let { EventHintBundle(it, relays().firstOrNull(), null) } + fun toEventHint() = event?.let { EventHintBundle(it, relayHintUrl(), null) } - fun toEventId() = EventIdHint(idHex, relays().firstOrNull()) + fun toEventId() = EventIdHintOptional(idHex, relayHintUrl()) fun updateChannelInfo( creator: User, @@ -99,7 +103,7 @@ class PublicChatChannel( fun updateChannelInfo( creator: User, - channelInfo: ChannelData, + channelInfo: ChannelDataNorm, updatedAt: Long, ) { this.info = channelInfo @@ -130,10 +134,12 @@ class LiveActivitiesChannel( fun address() = address - override fun relays() = info?.allRelayUrls() ?: super.relays() + override fun relays() = info?.allRelayUrls()?.toSet() ?: super.relays() fun relayHintUrl() = relays().firstOrNull() + fun relayHintUrls() = relays().take(3) + fun updateChannelInfo( creator: User, channelInfo: LiveActivitiesEvent, @@ -150,11 +156,10 @@ class LiveActivitiesChannel( override fun profilePicture(): String? = info?.image()?.ifBlank { null } override fun anyNameStartsWith(prefix: String): Boolean = - listOfNotNull(info?.title(), info?.summary()) - .filter { it.contains(prefix, true) } - .isNotEmpty() + info?.title()?.contains(prefix, true) == true || + info?.summary()?.contains(prefix, true) == true - fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, *relays().toTypedArray()) + fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrls()) fun toATag() = ATag(address, relayHintUrl()) @@ -173,7 +178,8 @@ abstract class Channel( var updatedMetadataAt: Long = 0 val notes = LargeCache() var lastNoteCreatedAt: Long = 0 - private var relays = mapOf() + + private var relays = mapOf() open fun idNote() = Hex.decode(idHex).toNEvent() @@ -187,13 +193,13 @@ abstract class Channel( open fun profilePicture(): String? = creator?.info?.banner - open fun relays() = + open fun relays(): Set = relays.keys .toSortedSet { o1, o2 -> val o1Count = relays[o1]?.number ?: 0 val o2Count = relays[o2]?.number ?: 0 o2Count.compareTo(o1Count) // descending - }.map { it.url } + } open fun updateChannelInfo( creator: User, @@ -206,13 +212,13 @@ abstract class Channel( } @Synchronized - fun addRelaySync(briefInfo: RelayBriefInfoCache.RelayBriefInfo) { + fun addRelaySync(briefInfo: NormalizedRelayUrl) { if (briefInfo !in relays) { relays = relays + Pair(briefInfo, Counter(1)) } } - fun addRelay(relay: RelayBriefInfoCache.RelayBriefInfo) { + fun addRelay(relay: NormalizedRelayUrl) { val counter = relays[relay] if (counter != null) { counter.number++ @@ -223,7 +229,7 @@ abstract class Channel( fun addNote( note: Note, - relay: RelayBriefInfoCache.RelayBriefInfo? = null, + relay: NormalizedRelayUrl? = null, ) { notes.put(note.idHex, note) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt new file mode 100644 index 0000000000..ec6a49a481 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model + +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer + +object Constants { + val nos = RelayUrlNormalizer.normalize("wss://nos.lol") + val mom = RelayUrlNormalizer.normalize("wss://nostr.mom") + val primal = RelayUrlNormalizer.normalize("wss://relay.primal.net") + val damus = RelayUrlNormalizer.normalize("wss://relay.damus.io") + val wine = RelayUrlNormalizer.normalize("wss://nostr.wine") + val band = RelayUrlNormalizer.normalize("wss://relay.nostr.band") + + val where = RelayUrlNormalizer.normalize("wss://relay.noswhere.com") + val elites = RelayUrlNormalizer.normalize("wss://nostrelites.org") + + val bitcoiner = RelayUrlNormalizer.normalize("wss://nostr.bitcoiner.social") + val bg = RelayUrlNormalizer.normalize("wss://relay.nostr.bg") + val oxtr = RelayUrlNormalizer.normalize("wss://nostr.oxtr.dev") + val fmtwiz = RelayUrlNormalizer.normalize("wss://nostr.fmt.wiz.biz") + + val nostoday = RelayUrlNormalizer.normalize("wss://search.nos.today") + + val auth = RelayUrlNormalizer.normalize("wss://auth.nostr1.com") + val oxchat = RelayUrlNormalizer.normalize("wss://relay.0xchat.com") + + val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr, fmtwiz, bg) + + val defaultSearchRelaySet = setOf(band, wine, where) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt index 421462e607..a78f944ef3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt @@ -50,19 +50,21 @@ import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment import com.vitorpamplona.amethyst.ui.components.HashTag import com.vitorpamplona.amethyst.ui.components.RenderRegular import com.vitorpamplona.amethyst.ui.navigation.EmptyNav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList @Preview @Composable fun RenderHashTagIconsPreview() { + val accountViewModel = mockAccountViewModel() ThemeComparisonColumn { RenderRegular( "Testing rendering of hashtags: #flowerstr #Bitcoin, #nostr, #lightning, #zap, #amethyst, #cashu, #plebs, #coffee, #skullofsatoshi, #grownostr, #footstr, #tunestr, #weed, #mate, #gamestr, #gamechain", EmptyTagList, ) { word, state -> when (word) { - is HashTagSegment -> HashTag(word, EmptyNav) + is HashTagSegment -> HashTag(word, accountViewModel, EmptyNav) is RegularTextSegment -> Text(word.segmentText) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 5825029032..a2977894f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -20,19 +20,16 @@ */ package com.vitorpamplona.amethyst.model -import android.R.attr.version import android.util.Log import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.dateFormatter import com.vitorpamplona.ammolite.relays.BundledInsert -import com.vitorpamplona.ammolite.relays.Relay -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.quartz.blossom.BlossomServersEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent @@ -56,7 +53,15 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.tagValueContains +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.mapTaggedAddress @@ -86,6 +91,14 @@ import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip19Bech32.decodeEventIdAsHexOrNull import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull +import com.vitorpamplona.quartz.nip19Bech32.entities.Entity +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay +import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip19Bech32.isATag import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent @@ -118,6 +131,8 @@ import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent +import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent @@ -135,13 +150,13 @@ import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityListEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent +import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent @@ -154,6 +169,7 @@ import com.vitorpamplona.quartz.nip90Dvms.NIP90UserDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.LargeCache import com.vitorpamplona.quartz.utils.TimeUtils @@ -175,11 +191,13 @@ import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.concurrent.ConcurrentHashMap +import kotlin.compareTo +import kotlin.text.get interface ILocalCache { fun markAsSeen( - string: String, - relay: RelayBriefInfoCache.RelayBriefInfo, + eventId: String, + relay: NormalizedRelayUrl, ) {} } @@ -188,10 +206,16 @@ object LocalCache : ILocalCache { val users = LargeCache() val notes = LargeCache() - val addressables = LargeCache() - val channels = LargeCache() + val addressables = LargeCache() + + val publicChatChannels = LargeCache() + val liveChatChannels = LargeCache() + val ephemeralChannels = LargeCache() + val awaitingPaymentRequests = ConcurrentHashMap Unit>>(10) + val relayHints = HintIndexer() + val deletionIndex = DeletionIndex() val observablesByKindAndETag = ConcurrentHashMap>>(10) @@ -287,17 +311,19 @@ object LocalCache : ILocalCache { return users.get(key) } - fun getAddressableNoteIfExists(key: String): AddressableNote? = addressables.get(key) + fun getAddressableNoteIfExists(key: String): AddressableNote? = Address.parse(key)?.let { addressables.get(it) } - fun getAddressableNoteIfExists(address: Address): AddressableNote? = getAddressableNoteIfExists(address.toValue()) + fun getAddressableNoteIfExists(address: Address): AddressableNote? = getAddressableNoteIfExists(address) - fun getNoteIfExists(key: String): Note? = addressables.get(key) ?: notes.get(key) + fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) } fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId) - fun getChannelIfExists(key: String): Channel? = channels.get(key) + fun getChannelIfExists(key: String): PublicChatChannel? = publicChatChannels.get(key) - fun getChannelIfExists(key: RoomId): Channel? = channels.get(key.toKey()) + fun getChannelIfExists(key: RoomId): EphemeralChatChannel? = ephemeralChannels.get(key) + + fun getChannelIfExists(key: Address): LiveActivitiesChannel? = liveChatChannels.get(key) fun getNoteIfExists(event: Event): Note? = if (event is AddressableEvent) { @@ -372,17 +398,18 @@ object LocalCache : ILocalCache { } } - fun getOrCreateChannel( - key: String, - channelFactory: (String) -> Channel, - ): Channel { - checkNotInMainThread() + fun getOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = + publicChatChannels.getOrCreate(key) { + PublicChatChannel(key) + } - return channels.getOrCreate(key, channelFactory) - } + fun getOrCreateLiveChannel(key: Address): LiveActivitiesChannel = + liveChatChannels.getOrCreate(key) { + LiveActivitiesChannel(key) + } - fun checkGetOrCreateChannel(key: RoomId): Channel? = - channels.getOrCreate(key.toKey()) { + fun getOrCreateEphemeralChannel(key: RoomId): EphemeralChatChannel = + ephemeralChannels.getOrCreate(key) { EphemeralChatChannel(key) } @@ -390,19 +417,23 @@ object LocalCache : ILocalCache { checkNotInMainThread() if (key.contains("@")) { - return channels.getOrCreate(key) { - val idParts = key.split("@") - EphemeralChatChannel(RoomId(idParts[0], idParts[1])) + val idParts = key.split("@") + val relay = RelayUrlNormalizer.normalizeOrNull(idParts[1]) + + if (relay == null) { + return null + } else { + getOrCreateEphemeralChannel(RoomId(idParts[0], relay)) } } if (isValidHex(key)) { - return channels.getOrCreate(key) { PublicChatChannel(key) } + return getOrCreatePublicChatChannel(key) } val address = Address.parse(key) if (address != null) { - return channels.getOrCreate(address.toValue()) { LiveActivitiesChannel(address) } + return getOrCreateLiveChannel(address) } return null } @@ -427,10 +458,7 @@ object LocalCache : ILocalCache { null } - fun getOrCreateAddressableNoteInternal(key: Address): AddressableNote = - addressables.getOrCreate(key.toValue()) { - AddressableNote(key) - } + fun getOrCreateAddressableNoteInternal(key: Address): AddressableNote = addressables.getOrCreate(key) { AddressableNote(key) } fun getOrCreateAddressableNote(key: Address): AddressableNote { val note = getOrCreateAddressableNoteInternal(key) @@ -449,16 +477,15 @@ object LocalCache : ILocalCache { note.author = checkGetOrCreateUser(possibleAuthor) } val relayHint = key.relay - if (!relayHint.isNullOrBlank()) { - val relay = RelayBriefInfoCache.get(RelayUrlFormatter.normalize(relayHint)) - note.addRelay(relay) + if (relayHint != null) { + note.addRelay(relayHint) } return note } fun consume( event: MetadataEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { // new event @@ -473,8 +500,8 @@ object LocalCache : ILocalCache { oldUser.updateUserInfo(newUserMetadata, event) if (relay != null) { oldUser.addRelayBeingUsed(relay, event.createdAt) - if (!RelayUrlFormatter.isLocalHost(relay.url)) { - oldUser.latestMetadataRelay = relay.url + if (!relay.isLocalHost()) { + oldUser.latestMetadataRelay = relay } } @@ -487,7 +514,7 @@ object LocalCache : ILocalCache { fun consume( event: ContactListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val user = getOrCreateUser(event.pubKey) @@ -507,7 +534,7 @@ object LocalCache : ILocalCache { fun consume( event: BookmarkListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val user = getOrCreateUser(event.pubKey) @@ -531,37 +558,37 @@ object LocalCache : ILocalCache { fun consume( event: TextNoteEvent, - relay: RelayBriefInfoCache.RelayBriefInfo? = null, + relay: NormalizedRelayUrl? = null, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: TorrentEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: InteractiveStoryPrologueEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: InteractiveStorySceneEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: InteractiveStoryReadingStateEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consumeRegularEvent( event: Event, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -597,67 +624,73 @@ object LocalCache : ILocalCache { fun consume( event: PictureEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: TorrentCommentEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: NIP90ContentDiscoveryResponseEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: NIP90ContentDiscoveryRequestEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: NIP90StatusEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: NIP90UserDiscoveryResponseEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: NIP90UserDiscoveryRequestEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeRegularEvent(event, relay, wasVerified) + + fun consume( + event: GoalEvent, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: GitPatchEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: GitIssueEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: GitReplyEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: LongTextNoteEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val version = getOrCreateNote(event.id) @@ -702,7 +735,7 @@ object LocalCache : ILocalCache { fun consume( event: WikiNoteEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val version = getOrCreateNote(event.id) @@ -806,13 +839,13 @@ object LocalCache : ILocalCache { fun consume( event: PollNoteEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) private fun consume( event: LiveActivitiesEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val version = getOrCreateNote(event.id) @@ -833,15 +866,15 @@ object LocalCache : ILocalCache { if (event.createdAt > (note.createdAt() ?: 0) && (isVerified || justVerify(event))) { note.loadEvent(event, author, emptyList()) - val channel = getOrCreateChannel(note.idHex) { LiveActivitiesChannel(note.address) } as? LiveActivitiesChannel + val channel = getOrCreateLiveChannel(note.address) if (relay != null) { - channel?.addRelay(relay) + channel.addRelay(relay) } val creator = event.host()?.let { checkGetOrCreateUser(it.pubKey) } ?: author - channel?.updateChannelInfo(creator, event, event.createdAt) + channel.updateChannelInfo(creator, event, event.createdAt) refreshObservers(note) @@ -853,139 +886,151 @@ object LocalCache : ILocalCache { fun consume( event: MuteListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: CommunityListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: GitRepositoryEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: ChannelListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: BlossomServersEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: FileServersEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: PeopleListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: EphemeralChatListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: FollowListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: AdvertisedRelayListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: ChatMessageRelayListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: PrivateOutboxRelayListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + + private fun consume( + event: HashtagListEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + + private fun consume( + event: GeohashListEvent, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: SearchRelayListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: CommunityDefinitionEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: EmojiPackSelectionEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: EmojiPackEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: ClassifiedsEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: PinListEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: RelaySetEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: AudioTrackEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: VideoVerticalEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: VideoHorizontalEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: StatusEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val version = getOrCreateNote(event.id) @@ -1019,13 +1064,13 @@ object LocalCache : ILocalCache { fun consume( event: RelationshipStatusEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: OtsEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val version = getOrCreateNote(event.id) @@ -1049,61 +1094,61 @@ object LocalCache : ILocalCache { fun consume( event: BadgeDefinitionEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: BadgeProfilesEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: BadgeAwardEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) private fun consume( event: NNSEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: AppDefinitionEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: CalendarEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: CalendarDateSlotEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: CalendarTimeSlotEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consume( event: CalendarRSVPEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) private fun consumeBaseReplaceable( event: BaseAddressableEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val version = getOrCreateNote(event.id) @@ -1140,19 +1185,19 @@ object LocalCache : ILocalCache { fun consume( event: AppRecommendationEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: AppSpecificDataEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) fun consume( event: PrivateDmEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1190,7 +1235,7 @@ object LocalCache : ILocalCache { fun consume( event: DeletionEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { if (deletionIndex.add(event, wasVerified)) { @@ -1207,7 +1252,7 @@ object LocalCache : ILocalCache { // Counts the replies deleteNote(addressableNote) - addressables.remove(addressableNote.idHex) + addressables.remove(addressableNote.address) deletedAtLeastOne = true } @@ -1233,7 +1278,7 @@ object LocalCache : ILocalCache { // Counts the replies deleteNote(deleteNote) - addressables.remove(deleteNote.idHex) + addressables.remove(deleteNote.address) deletedAtLeastOne = true } @@ -1340,7 +1385,7 @@ object LocalCache : ILocalCache { fun consume( event: RepostEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1358,7 +1403,7 @@ object LocalCache : ILocalCache { repliesTo.forEach { it.addBoost(note) } event.containedPost()?.let { - justConsumeInner(it, relay, false) + justConsumeAndUpdateIndexes(it, relay, false) } refreshObservers(note) @@ -1370,7 +1415,7 @@ object LocalCache : ILocalCache { fun consume( event: GenericRepostEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1388,7 +1433,7 @@ object LocalCache : ILocalCache { repliesTo.forEach { it.addBoost(note) } event.containedPost()?.let { - justConsumeInner(it, relay, false) + justConsumeAndUpdateIndexes(it, relay, false) } refreshObservers(note) @@ -1401,7 +1446,7 @@ object LocalCache : ILocalCache { fun consume( event: CommunityPostApprovalEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1423,7 +1468,7 @@ object LocalCache : ILocalCache { repliesTo.forEach { it.addBoost(note) } event.containedPost()?.let { - justConsumeInner(it, relay, false) + justConsumeAndUpdateIndexes(it, relay, false) } refreshObservers(note) @@ -1436,7 +1481,7 @@ object LocalCache : ILocalCache { fun consume( event: ReactionEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1462,7 +1507,7 @@ object LocalCache : ILocalCache { fun consume( event: ReportEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1506,11 +1551,11 @@ object LocalCache : ILocalCache { fun consume( event: ChannelCreateEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { // Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") - val oldChannel = getOrCreateChannel(event.id) { PublicChatChannel(it) } + val oldChannel = getOrCreatePublicChatChannel(event.id) val author = getOrCreateUser(event.pubKey) val note = getOrCreateNote(event.id) @@ -1530,7 +1575,7 @@ object LocalCache : ILocalCache { } if (oldChannel.creator == null || oldChannel.creator == author) { - if (oldChannel is PublicChatChannel && (isVerified || justVerify(event))) { + if (isVerified || justVerify(event)) { oldChannel.updateChannelInfo(author, event) } } @@ -1540,7 +1585,7 @@ object LocalCache : ILocalCache { fun consume( event: ChannelMetadataEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val channelId = event.channelId() @@ -1575,7 +1620,7 @@ object LocalCache : ILocalCache { fun consume( event: ChannelMessageEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val channelId = event.channelId() @@ -1620,14 +1665,14 @@ object LocalCache : ILocalCache { fun consume( event: EphemeralChatEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { - val relayUrl = event.relay().ifBlank { return false } + val roomId = event.roomId() + if (roomId == null) return false - val channelId = RoomId(event.room(), relayUrl) - - val channel = checkGetOrCreateChannel(channelId) ?: return false + val channelId = roomId + val channel = getOrCreateEphemeralChannel(channelId) ?: return false val note = getOrCreateNote(event.id) channel.addNote(note, relay) @@ -1659,18 +1704,18 @@ object LocalCache : ILocalCache { fun consume( event: CommentEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: LiveActivitiesChatMessageEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val activityAddress = event.activityAddress() ?: return false - val channel = getOrCreateChannel(activityAddress.toValue()) { LiveActivitiesChannel(activityAddress) } + val channel = getOrCreateLiveChannel(activityAddress) val note = getOrCreateNote(event.id) channel.addNote(note, relay) @@ -1708,20 +1753,20 @@ object LocalCache : ILocalCache { @Suppress("UNUSED_PARAMETER") fun consume( event: ChannelHideMessageEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean = false @Suppress("UNUSED_PARAMETER") fun consume( event: ChannelMuteUserEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean = false fun consume( event: LnZapEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1734,7 +1779,7 @@ object LocalCache : ILocalCache { if (existingZapRequest == null || existingZapRequest.event == null) { // tries to add it event.zapRequest?.let { - justConsumeInner(it, relay, false) + justConsumeAndUpdateIndexes(it, relay, false) } } @@ -1764,7 +1809,7 @@ object LocalCache : ILocalCache { fun consume( event: LnZapRequestEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1792,37 +1837,37 @@ object LocalCache : ILocalCache { fun consume( event: AudioHeaderEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: FileHeaderEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: ProfileGalleryEntryEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: FileStorageHeaderEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: FhirResourceEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: TextNoteModificationEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1857,13 +1902,13 @@ object LocalCache : ILocalCache { fun consume( event: HighlightEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: FileStorageEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1885,7 +1930,7 @@ object LocalCache : ILocalCache { stream.close() Log.i( "FileStorageEvent", - "NIP95 File received from ${relay?.url} and saved to disk as $file", + "NIP95 File received from $relay and saved to disk as $file", ) true } else { @@ -1916,7 +1961,7 @@ object LocalCache : ILocalCache { private fun consume( event: ChatMessageEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -1966,7 +2011,7 @@ object LocalCache : ILocalCache { private fun consume( event: ChatMessageEncryptedFileHeaderEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -2016,7 +2061,7 @@ object LocalCache : ILocalCache { fun consume( event: SealedRumorEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -2041,7 +2086,7 @@ object LocalCache : ILocalCache { fun consume( event: GiftWrapEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val note = getOrCreateNote(event.id) @@ -2065,7 +2110,7 @@ object LocalCache : ILocalCache { fun consume( event: LnZapPaymentRequestEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { // Does nothing without a response callback. @@ -2076,6 +2121,7 @@ object LocalCache : ILocalCache { event: LnZapPaymentRequestEvent, zappedNote: Note?, wasVerified: Boolean, + relay: NormalizedRelayUrl?, onResponse: (LnZapPaymentResponseEvent) -> Unit, ): Boolean { val note = getOrCreateNote(event.id) @@ -2087,6 +2133,10 @@ object LocalCache : ILocalCache { if (wasVerified || justVerify(event)) { note.loadEvent(event, author, emptyList()) + relay?.let { + note.addRelay(relay) + } + zappedNote?.addZapPayment(note, null) awaitingPaymentRequests.put(event.id, Pair(zappedNote, onResponse)) @@ -2101,7 +2151,7 @@ object LocalCache : ILocalCache { fun consume( event: LnZapPaymentResponseEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { val requestId = event.requestId() @@ -2154,7 +2204,7 @@ object LocalCache : ILocalCache { user.pubkeyHex.startsWith(username, true) || user.pubkeyNpub().startsWith(username, true) ) && - (forAccount == null || (!forAccount.isHidden(user) && !user.containsAny(forAccount.flowHiddenUsers.value.hiddenWordsCase))) + (forAccount == null || (!forAccount.isHidden(user) && !user.containsAny(forAccount.hiddenUsers.flow.value.hiddenWordsCase))) } return finds.sortedWith( @@ -2184,7 +2234,7 @@ object LocalCache : ILocalCache { fun findNotesStartingWith( text: String, - forAccount: Account, + hiddenUsers: HiddenUsersState, ): List { checkNotInMainThread() @@ -2207,7 +2257,7 @@ object LocalCache : ILocalCache { if (note.event?.tags?.tagValueContains(text, true) == true || note.idHex.startsWith(text, true) ) { - if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) { + if (!note.isHiddenFor(hiddenUsers.flow.value)) { return@filter true } else { return@filter false @@ -2215,7 +2265,7 @@ object LocalCache : ILocalCache { } if (note.event?.isContentEncoded() == false) { - if (!note.isHiddenFor(forAccount.flowHiddenUsers.value)) { + if (!note.isHiddenFor(hiddenUsers.flow.value)) { return@filter note.event?.content?.contains(text, true) ?: false } else { return@filter false @@ -2232,7 +2282,7 @@ object LocalCache : ILocalCache { if (addressable.event?.tags?.tagValueContains(text, true) == true || addressable.idHex.startsWith(text, true) ) { - if (!addressable.isHiddenFor(forAccount.flowHiddenUsers.value)) { + if (!addressable.isHiddenFor(hiddenUsers.flow.value)) { return@filter true } else { return@filter false @@ -2240,7 +2290,7 @@ object LocalCache : ILocalCache { } if (addressable.event?.isContentEncoded() == false) { - if (!addressable.isHiddenFor(forAccount.flowHiddenUsers.value)) { + if (!addressable.isHiddenFor(hiddenUsers.flow.value)) { return@filter addressable.event?.content?.contains(text, true) ?: false } else { return@filter false @@ -2261,11 +2311,21 @@ object LocalCache : ILocalCache { return listOfNotNull(getChannelIfExists(key)) } - return channels.filter { _, channel -> + return publicChatChannels.filter { _, channel -> channel.anyNameStartsWith(text) || channel.idHex.startsWith(text, true) || channel.idNote().startsWith(text, true) - } + } + + ephemeralChannels.filter { _, channel -> + channel.anyNameStartsWith(text) || + channel.idHex.startsWith(text, true) || + channel.idNote().startsWith(text, true) + } + + liveChatChannels.filter { _, channel -> + channel.anyNameStartsWith(text) || + channel.idHex.startsWith(text, true) || + channel.idNote().startsWith(text, true) + } } suspend fun findStatusesForUser(user: User): ImmutableList { @@ -2358,49 +2418,76 @@ object LocalCache : ILocalCache { } } + fun pruneHiddenMessagesChannel( + channel: Channel, + account: Account, + ) { + val toBeRemoved = channel.pruneHiddenMessages(account) + + val childrenToBeRemoved = mutableListOf() + + toBeRemoved.forEach { + removeFromCache(it) + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) + } + + removeFromCache(childrenToBeRemoved) + + if (toBeRemoved.size > 100 || channel.notes.size() > 100) { + println( + "PRUNE: ${toBeRemoved.size} hidden messages removed from ${channel.toBestDisplayName()}. ${channel.notes.size()} kept", + ) + } + } + fun pruneHiddenMessages(account: Account) { - channels.forEach { _, channel -> - val toBeRemoved = channel.pruneHiddenMessages(account) + ephemeralChannels.forEach { _, channel -> + pruneHiddenMessagesChannel(channel, account) + } - val childrenToBeRemoved = mutableListOf() + liveChatChannels.forEach { _, channel -> + pruneHiddenMessagesChannel(channel, account) + } - toBeRemoved.forEach { - removeFromCache(it) + publicChatChannels.forEach { _, channel -> + pruneHiddenMessagesChannel(channel, account) + } + } - childrenToBeRemoved.addAll(it.removeAllChildNotes()) - } + fun pruneOldMessagesChannel(channel: Channel) { + val toBeRemoved = channel.pruneOldMessages() - removeFromCache(childrenToBeRemoved) + val childrenToBeRemoved = mutableListOf() - if (toBeRemoved.size > 100 || channel.notes.size() > 100) { - println( - "PRUNE: ${toBeRemoved.size} hidden messages removed from ${channel.toBestDisplayName()}. ${channel.notes.size()} kept", - ) - } + toBeRemoved.forEach { + removeFromCache(it) + + childrenToBeRemoved.addAll(it.removeAllChildNotes()) + } + + removeFromCache(childrenToBeRemoved) + + if (toBeRemoved.size > 100 || channel.notes.size() > 100) { + println( + "PRUNE: ${toBeRemoved.size} old messages removed from ${channel.toBestDisplayName()}. ${channel.notes.size()} kept", + ) } } fun pruneOldMessages() { checkNotInMainThread() - channels.forEach { _, channel -> - val toBeRemoved = channel.pruneOldMessages() + ephemeralChannels.forEach { _, channel -> + pruneOldMessagesChannel(channel) + } - val childrenToBeRemoved = mutableListOf() + liveChatChannels.forEach { _, channel -> + pruneOldMessagesChannel(channel) + } - toBeRemoved.forEach { - removeFromCache(it) - - childrenToBeRemoved.addAll(it.removeAllChildNotes()) - } - - removeFromCache(childrenToBeRemoved) - - if (toBeRemoved.size > 100 || channel.notes.size() > 100) { - println( - "PRUNE: ${toBeRemoved.size} old messages removed from ${channel.toBestDisplayName()}. ${channel.notes.size()} kept", - ) - } + publicChatChannels.forEach { _, channel -> + pruneOldMessagesChannel(channel) } users.forEach { _, user -> @@ -2452,7 +2539,7 @@ object LocalCache : ILocalCache { val noteEvent = note.event if (noteEvent is AddressableEvent) { noteEvent.createdAt < - (addressables.get(noteEvent.aTag().toTag())?.event?.createdAt ?: 0) + (addressables.get(noteEvent.address())?.event?.createdAt ?: 0) } else { false } @@ -2461,7 +2548,7 @@ object LocalCache : ILocalCache { val childrenToBeRemoved = mutableListOf() toBeRemoved.forEach { - val newerVersion = (it.event as? AddressableEvent)?.aTag()?.toTag()?.let { tag -> addressables.get(tag) } + val newerVersion = (it.event as? AddressableEvent)?.address()?.let { tag -> addressables.get(tag) } if (newerVersion != null) { it.moveAllReferencesTo(newerVersion) } @@ -2579,7 +2666,7 @@ object LocalCache : ILocalCache { val childrenToBeRemoved = mutableListOf() val toBeRemoved = - account.flowHiddenUsers.value.hiddenUsers + account.hiddenUsers.flow.value.hiddenUsers .map { userHex -> (notes.filter { _, it -> it.event?.pubKey == userHex } + addressables.filter { _, it -> it.event?.pubKey == userHex }).toSet() }.flatten() @@ -2614,7 +2701,7 @@ object LocalCache : ILocalCache { override fun markAsSeen( eventId: String, - relay: RelayBriefInfoCache.RelayBriefInfo, + relay: NormalizedRelayUrl, ) { val note = getNoteIfExists(eventId) @@ -2655,7 +2742,7 @@ object LocalCache : ILocalCache { fun consume( event: DraftEvent, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { if (!event.isDeleted()) { @@ -2722,7 +2809,9 @@ object LocalCache : ILocalCache { } } is EphemeralChatEvent -> { - checkGetOrCreateChannel(draft.roomId().toKey())?.addNote(note, null) + draft.roomId()?.toKey()?.let { + checkGetOrCreateChannel(it)?.addNote(note, null) + } } is ChannelMessageEvent -> { draft.channelId()?.let { channelId -> @@ -2802,7 +2891,9 @@ object LocalCache : ILocalCache { } } is EphemeralChatEvent -> { - checkGetOrCreateChannel(draft.roomId())?.removeNote(draftWrap) + draft.roomId()?.let { + getOrCreateEphemeralChannel(it).removeNote(draftWrap) + } } is TextNoteEvent -> { val replyTo = computeReplyTo(draft) @@ -2811,11 +2902,45 @@ object LocalCache : ILocalCache { } } - fun justConsumeMyOwnEvent(event: Event) = justConsumeInner(event, null, true) + fun consume(nip19: Entity) { + when (nip19) { + is NSec -> getOrCreateUser(nip19.toPubKeyHex()) + is NPub -> getOrCreateUser(nip19.hex) + is NProfile -> { + nip19.relay.forEach { relayHint -> + relayHints.addKey(nip19.hex, relayHint) + } + getOrCreateUser(nip19.hex) + } + is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> { + getOrCreateNote(nip19.hex) + } + is NEvent -> { + nip19.relay.forEach { relayHint -> + relayHints.addEvent(nip19.hex, relayHint) + } + getOrCreateNote(nip19.hex) + } + is NEmbed -> { + justConsume(nip19.event, null, false) + } + is NRelay -> {} + is NAddress -> { + val aTag = nip19.aTag() + nip19.relay.forEach { relayHint -> + relayHints.addAddress(aTag, relayHint) + } + getOrCreateAddressableNote(nip19.address()) + } + else -> { } + } + } + + fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true) fun justConsume( event: Event, - relay: Relay?, + relay: IRelayClient?, wasVerified: Boolean, ): Boolean { if (deletionIndex.hasBeenDeleted(event)) { @@ -2833,7 +2958,7 @@ object LocalCache : ILocalCache { // updates relay with a new event. getAddressableNoteIfExists(event.addressTag())?.let { note -> note.event?.let { existingEvent -> - if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay.brief)) { + if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay.url)) { Log.d("LocalCache", "Updating ${relay.url} with a new version of ${event.toJson()} to ${existingEvent.toJson()}") relay.send(existingEvent) } @@ -2841,12 +2966,44 @@ object LocalCache : ILocalCache { } } - return justConsumeInner(event, relay?.brief, wasVerified) + return justConsumeAndUpdateIndexes(event, relay?.url, wasVerified) } - fun justConsumeInner( + private fun justConsumeAndUpdateIndexes( event: Event, - relay: RelayBriefInfoCache.RelayBriefInfo?, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val wasNew = justConsumeInnerInner(event, relay, wasVerified) + + if (wasNew && relay != null) { + updateHintIndexes(event) + } + + return wasNew + } + + fun updateHintIndexes(event: Event) { + if (event is EventHintProvider) { + event.eventHints().forEach { + relayHints.addEvent(it.eventId, it.relay) + } + } + if (event is AddressHintProvider) { + event.addressHints().forEach { + relayHints.addAddress(it.addressId, it.relay) + } + } + if (event is PubKeyHintProvider) { + event.pubKeyHints().forEach { + relayHints.addKey(it.pubkey, it.relay) + } + } + } + + private fun justConsumeInnerInner( + event: Event, + relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean = try { @@ -2895,11 +3052,14 @@ object LocalCache : ILocalCache { is FileStorageEvent -> consume(event, relay, wasVerified) is FileStorageHeaderEvent -> consume(event, relay, wasVerified) is FollowListEvent -> consume(event, relay, wasVerified) + is GeohashListEvent -> consume(event, relay, wasVerified) + is GoalEvent -> consume(event, relay, wasVerified) is GiftWrapEvent -> consume(event, relay, wasVerified) is GitIssueEvent -> consume(event, relay, wasVerified) is GitReplyEvent -> consume(event, relay, wasVerified) is GitPatchEvent -> consume(event, relay, wasVerified) is GitRepositoryEvent -> consume(event, relay, wasVerified) + is HashtagListEvent -> consume(event, relay, wasVerified) is HighlightEvent -> consume(event, relay, wasVerified) is InteractiveStoryPrologueEvent -> consume(event, relay, wasVerified) is InteractiveStorySceneEvent -> consume(event, relay, wasVerified) @@ -2954,7 +3114,7 @@ object LocalCache : ILocalCache { fun hasConsumed(notificationEvent: Event): Boolean = if (notificationEvent is AddressableEvent) { - val note = addressables.get(notificationEvent.addressTag()) + val note = addressables.get(notificationEvent.address()) val noteEvent = note?.event noteEvent != null && notificationEvent.createdAt <= noteEvent.createdAt } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 97b7c0c7ba..3cb6373292 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -22,13 +22,11 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.launchAndWaitAll +import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.service.replace -import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.amethyst.ui.note.toShortenHex -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent @@ -37,6 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -74,6 +73,8 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.containsAny +import com.vitorpamplona.quartz.utils.launchAndWaitAll +import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -157,7 +158,7 @@ open class Note( var zapPayments = mapOf() private set - var relays = listOf() + var relays = listOf() private set fun id() = Hex.decode(idHex) @@ -183,14 +184,26 @@ open class Note( } } - fun relayHintUrl(): String? { + fun relayUrls(): List { + val authorRelay = author?.relayHints()?.ifEmpty { null } + + return authorRelay ?: relays + } + + fun relayUrlsForReactions(): List { + val authorRelay = author?.inboxRelays()?.ifEmpty { null } + + return authorRelay ?: relays + } + + fun relayHintUrl(): NormalizedRelayUrl? { val authorRelay = author?.latestMetadataRelay return if (relays.isNotEmpty()) { - if (authorRelay != null && relays.any { it.url == authorRelay }) { + if (authorRelay != null && relays.any { it == authorRelay }) { authorRelay } else { - relays.firstOrNull()?.url + relays.firstOrNull() } } else { null @@ -293,7 +306,7 @@ open class Note( zaps = mapOf() zapPayments = mapOf() zapsAmount = BigDecimal.ZERO - relays = listOf() + relays = listOf() if (repliesChanged) flowSet?.replies?.invalidateData() if (reactionsChanged) flowSet?.reactions?.invalidateData() @@ -445,17 +458,17 @@ open class Note( } @Synchronized - fun addRelaySync(briefInfo: RelayBriefInfoCache.RelayBriefInfo) { - if (briefInfo !in relays) { - relays = relays + briefInfo + fun addRelaySync(relay: NormalizedRelayUrl) { + if (relay !in relays) { + relays = relays + relay } } - fun hasRelay(relay: RelayBriefInfoCache.RelayBriefInfo) = relay !in relays + fun hasRelay(relay: NormalizedRelayUrl) = relay !in relays - fun addRelay(brief: RelayBriefInfoCache.RelayBriefInfo) { - if (brief !in relays) { - addRelaySync(brief) + fun addRelay(relay: NormalizedRelayUrl) { + if (relay !in relays) { + addRelaySync(relay) flowSet?.relays?.invalidateData() } } @@ -779,9 +792,17 @@ open class Note( fun reactedBy(loggedIn: User): List = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key } fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { - return boosts.firstOrNull { - it.author == loggedIn && (it.createdAt() ?: 0) > TimeUtils.fiveMinutesAgo() - } != null // 5 minute protection + val fiveMinsAgo = TimeUtils.fiveMinutesAgo() + return boosts.any { + it.author == loggedIn && (it.createdAt() ?: 0) > fiveMinsAgo + } + } + + fun hasBoostedInTheLast5Minutes(loggedIn: HexKey): Boolean { + val fiveMinsAgo = TimeUtils.fiveMinutesAgo() + return boosts.any { + (it.createdAt() ?: 0) > fiveMinsAgo && it.author?.pubkeyHex == loggedIn + } } fun boostedBy(loggedIn: User): List = boosts.filter { it.author == loggedIn } @@ -823,7 +844,7 @@ open class Note( zapsAmount = BigDecimal.ZERO } - fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean { + fun isHiddenFor(accountChoices: HiddenUsersState.LiveHiddenUsers): Boolean { val thisEvent = event ?: return false val hash = thisEvent.pubKey.hashCode() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 8d2165304f..c633876cae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -24,11 +24,11 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.toShortenHex -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache.RelayBriefInfo import com.vitorpamplona.quartz.lightning.Lud06 import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash import com.vitorpamplona.quartz.nip01Core.tags.people.PTag @@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent @@ -57,7 +58,7 @@ class User( var info: UserMetadata? = null var latestMetadata: MetadataEvent? = null - var latestMetadataRelay: String? = null + var latestMetadataRelay: NormalizedRelayUrl? = null var latestContactList: ContactListEvent? = null var latestBookmarkList: BookmarkListEvent? = null @@ -67,7 +68,7 @@ class User( var zaps = mapOf() private set - var relaysBeingUsed = mapOf() + var relaysBeingUsed = mapOf() private set var privateChatrooms = mapOf() @@ -79,13 +80,21 @@ class User( fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex() + fun dmInboxRelayList() = (LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(pubkeyHex))?.event as? ChatMessageRelayListEvent) + fun authorRelayList() = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(pubkeyHex))?.event as? AdvertisedRelayListEvent) fun toNProfile() = NProfile.create(pubkeyHex, relayHints()) - fun relayHints() = authorRelayList()?.writeRelays()?.take(3) ?: listOfNotNull(latestMetadataRelay) + fun outboxRelays() = authorRelayList()?.writeRelaysNorm() ?: listOfNotNull(latestMetadataRelay) - fun bestRelayHint() = authorRelayList()?.writeRelays()?.firstOrNull() ?: latestMetadataRelay + fun relayHints() = authorRelayList()?.writeRelaysNorm()?.take(3) ?: listOfNotNull(latestMetadataRelay) + + fun inboxRelays() = authorRelayList()?.readRelaysNorm() ?: listOfNotNull(latestMetadataRelay) + + fun dmInboxRelays() = dmInboxRelayList()?.relays()?.ifEmpty { null } ?: inboxRelays() + + fun bestRelayHint() = authorRelayList()?.writeRelaysNorm()?.firstOrNull() ?: latestMetadataRelay fun toPTag() = PTag(pubkeyHex, bestRelayHint()) @@ -289,12 +298,12 @@ class User( } fun addRelayBeingUsed( - relay: RelayBriefInfo, + relay: NormalizedRelayUrl, eventTime: Long, ) { - val here = relaysBeingUsed[relay.url] + val here = relaysBeingUsed[relay] if (here == null) { - relaysBeingUsed = relaysBeingUsed + Pair(relay.url, RelayInfo(relay.url, eventTime, 1)) + relaysBeingUsed = relaysBeingUsed + Pair(relay, RelayInfo(relay, eventTime, 1)) } else { if (eventTime > here.lastEvent) { here.lastEvent = eventTime @@ -345,8 +354,7 @@ class User( type: ReportType, ): Boolean = reports[loggedIn]?.firstOrNull { - it.event is ReportEvent && - (it.event as ReportEvent).reportedAuthor().any { it.type == type } + (it.event as? ReportEvent)?.reportedAuthor()?.any { it.type == type } ?: false } != null fun containsAny(hiddenWordsCase: List): Boolean { @@ -445,7 +453,7 @@ class UserFlowSet( @Immutable data class RelayInfo( - val url: String, + val url: NormalizedRelayUrl, var lastEvent: Long, var counter: Long, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt new file mode 100644 index 0000000000..4cf071b460 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.edits + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class PrivateStorageRelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getPrivateOutboxRelayListAddress() = PrivateOutboxRelayListEvent.createAddress(signer.pubKey) + + fun getPrivateOutboxRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getPrivateOutboxRelayListAddress()) + + fun getPrivateOutboxRelayListFlow(): StateFlow = getPrivateOutboxRelayListNote().flow().metadata.stateFlow + + fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent + + fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set { + val event = note.event as? PrivateOutboxRelayListEvent ?: settings.backupPrivateHomeRelayList + return event?.relays()?.toSet() ?: emptySet() + } + + val flow = + getPrivateOutboxRelayListFlow() + .map { normalizePrivateOutboxRelayListWithBackup(it.note) } + .onStart { emit(normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + fun saveRelayList( + relays: List, + onDone: (PrivateOutboxRelayListEvent) -> Unit, + ) { + val relayListForPrivateOutbox = getPrivateOutboxRelayList() + + if (relayListForPrivateOutbox != null && !relayListForPrivateOutbox.cachedPrivateTags().isNullOrEmpty()) { + PrivateOutboxRelayListEvent.updateRelayList( + earlierVersion = relayListForPrivateOutbox, + relays = relays, + signer = signer, + onReady = onDone, + ) + } else { + PrivateOutboxRelayListEvent.createFromScratch( + relays = relays, + signer = signer, + onReady = onDone, + ) + } + } + + init { + settings.backupPrivateHomeRelayList?.let { event -> + Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + event.privateTags(signer) { + LocalCache.justConsumeMyOwnEvent(event) + } + } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Private Home Relay List Collector Start") + getPrivateOutboxRelayListFlow().collect { + Log.d("AccountRegisterObservers", "Updating Private Home Relay List for ${signer.pubKey}") + (it.note.event as? PrivateOutboxRelayListEvent)?.let { + settings.updatePrivateHomeRelayList(it) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt index 8cf9ecfb59..52f9422d31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt @@ -20,29 +20,36 @@ */ package com.vitorpamplona.amethyst.model.emphChat +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch import kotlin.coroutines.resume class EphemeralChatListState( val signer: NostrSigner, val cache: LocalCache, val scope: CoroutineScope, + val settings: AccountSettings, ) { fun getEphemeralChatListAddress() = EphemeralChatListEvent.createAddress(signer.pubKey) @@ -52,31 +59,27 @@ class EphemeralChatListState( fun getEphemeralChatList(): EphemeralChatListEvent? = getEphemeralChatListNote().event as? EphemeralChatListEvent + suspend fun ephemeralChatListWithBackup(note: Note): Set { + return ephemeralChatList( + note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList, + ) + } + + suspend fun ephemeralChatList(event: EphemeralChatListEvent?): Set { + return tryAndWait { continuation -> + event?.publicAndPrivateRoomIds(signer) { + continuation.resume(it) + } + } ?: emptySet() + } + @OptIn(ExperimentalCoroutinesApi::class) val liveEphemeralChatList: StateFlow> by lazy { getEphemeralChatListFlow() .transformLatest { noteState -> - val set = - tryAndWait { continuation -> - (noteState.note.event as? EphemeralChatListEvent)?.publicAndPrivateRoomIds(signer) { - continuation.resume(it) - } - } - - if (set != null) { - emit(set) - } + emit(ephemeralChatListWithBackup(noteState.note)) }.onStart { - val set = - tryAndWait { continuation -> - getEphemeralChatList()?.publicAndPrivateRoomIds(signer) { - continuation.resume(it) - } - } - - if (set != null) { - emit(set) - } + emit(ephemeralChatListWithBackup(getEphemeralChatListNote())) }.flowOn(Dispatchers.Default) .stateIn( scope, @@ -89,6 +92,7 @@ class EphemeralChatListState( channel: EphemeralChatChannel, onDone: (EphemeralChatListEvent) -> Unit, ) { + if (!signer.isWriteable()) return val ephemeralChatList = getEphemeralChatList() if (ephemeralChatList == null) { @@ -113,6 +117,7 @@ class EphemeralChatListState( channel: EphemeralChatChannel, onDone: (EphemeralChatListEvent) -> Unit, ) { + if (!signer.isWriteable()) return val ephemeralChatList = getEphemeralChatList() if (ephemeralChatList != null) { @@ -125,4 +130,26 @@ class EphemeralChatListState( ) } } + + init { + settings.backupEphemeralChatList?.let { event -> + Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + event.privateTags(signer) { + LocalCache.justConsumeMyOwnEvent(event) + } + } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "EphemeralChatList Collector Start") + getEphemeralChatListFlow().collect { + Log.d("AccountRegisterObservers", "EphemeralChatList List for ${signer.pubKey}") + (it.note.event as? EphemeralChatListEvent)?.let { + settings.updateEphemeralChatListTo(it) + } + } + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt new file mode 100644 index 0000000000..88a49482e2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.localRelays + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class LocalRelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun normalizeLocalRelayListWithBackup(relayList: Set): Set { + return relayList.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }.toSet() + } + + val flow = + settings.localRelayServers + .map { normalizeLocalRelayListWithBackup(it) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + normalizeLocalRelayListWithBackup(settings.localRelayServers.value), + ) + + fun saveRelayList( + relays: List, + onDone: () -> Unit, + ) { + settings.updateLocalRelayServers(relays.map { it.url }.toSet()) + onDone() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt new file mode 100644 index 0000000000..8db6d0c8d4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip01UserMetadata + +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn +import kotlin.collections.plus + +class AccountOutboxRelayState( + nip65: Nip65RelayListState, + privateStorage: PrivateStorageRelayListState, + local: LocalRelayListState, + scope: CoroutineScope, +) { + val flow = + combine( + nip65.outboxFlow, + privateStorage.flow, + local.flow, + ) { nip65Inbox, privateOutBox, localRelays -> + nip65Inbox + privateOutBox + localRelays + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + nip65.outboxFlow.value + + privateStorage.flow.value + + local.flow.value, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt similarity index 61% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayManager.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt index 0200cb0e58..1d05dee9bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/NotificationInboxRelayState.kt @@ -18,39 +18,32 @@ * 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.service.relays +package com.vitorpamplona.amethyst.model.nip01UserMetadata -import android.app.Application -import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager -import com.vitorpamplona.amethyst.ui.tor.TorManager +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn -/** - * There should be only one instance of the Tor binding per app. - * - * Tor will connect as soon as status is listened to. - */ -class RelayManager( - app: Application, +class NotificationInboxRelayState( + nip65RelayList: Nip65RelayListState, + localRelayList: LocalRelayListState, scope: CoroutineScope, - torManager: TorManager, - connManager: ConnectivityManager, ) { - val relayService = + val flow = combine( - torManager.status, - connManager.status, - ) { torStatus, connManager -> - } - - val status: StateFlow = - RelayService(app).status.stateIn( - scope, - SharingStarted.WhileSubscribed(30000), - RelayServiceStatus.Off, - ) + nip65RelayList.inboxFlow, + localRelayList.flow, + ) { nip65Inbox, localRelays -> + nip65Inbox + localRelays + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + nip65RelayList.inboxFlow.value + localRelayList.flow.value, + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt new file mode 100644 index 0000000000..dc4e9a2c96 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip01UserMetadata + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.UserState +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +class UserMetadataState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + // fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey) + + fun getUserMetadataUser(): User = cache.getOrCreateUser(signer.pubKey) + + fun getUserMetadataFlow(): StateFlow = getUserMetadataUser().flow().metadata.stateFlow + + fun getUserMetadataEvent(): MetadataEvent? = getUserMetadataUser().latestMetadata + + fun sendNewUserMetadata( + name: String? = null, + picture: String? = null, + banner: String? = null, + website: String? = null, + pronouns: String? = null, + about: String? = null, + nip05: String? = null, + lnAddress: String? = null, + lnURL: String? = null, + twitter: String? = null, + mastodon: String? = null, + github: String? = null, + onDone: (MetadataEvent) -> Unit, + ) { + val latest = getUserMetadataEvent() + + val template = + if (latest != null) { + MetadataEvent.updateFromPast( + latest = latest, + name = name, + displayName = name, + picture = picture, + banner = banner, + website = website, + pronouns = pronouns, + about = about, + nip05 = nip05, + lnAddress = lnAddress, + lnURL = lnURL, + twitter = twitter, + mastodon = mastodon, + github = github, + ) + } else { + MetadataEvent.createNew( + name = name, + displayName = name, + picture = picture, + banner = banner, + website = website, + pronouns = pronouns, + about = about, + nip05 = nip05, + lnAddress = lnAddress, + lnURL = lnURL, + twitter = twitter, + mastodon = mastodon, + github = github, + ) + } + + signer.sign(template, onDone) + } + + init { + settings.backupUserMetadata?.let { + Log.d("AccountRegisterObservers", "Loading saved user metadata ${it.toJson()}") + + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + } + + // saves contact list for the next time. + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Kind 0 Collector Start") + getUserMetadataFlow().collect { + Log.d("AccountRegisterObservers", "Updating Kind 0 ${it.user.toBestDisplayName()}") + settings.updateUserMetadata(it.user.latestMetadata) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt new file mode 100644 index 0000000000..ad63c5daf8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip02FollowLists + +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlin.collections.map +import kotlin.collections.toSet + +class FollowListOutboxRelays( + kind3Follows: FollowListState, + val cache: LocalCache, + scope: CoroutineScope, +) { + fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.createAddress(pubkey) + + fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey)) + + fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow + + fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent + + fun allRelayListFlows(followList: Set): List> = followList.map { getNIP65RelayListFlow(it) } + + fun combineAllFlows(flows: List>): Flow> = + combine(flows) { relayListNotes: Array -> + relayListNotes.mapNotNull { + (it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() + } + }.map { + it.flatten().toSet() + } + + @OptIn(ExperimentalCoroutinesApi::class) + val flow: StateFlow> = + kind3Follows.flow.transformLatest { followList -> + val flows: List> = allRelayListFlows(followList.authors) + val relayListFlows = combineAllFlows(flows) + emitAll(relayListFlows) + }.onStart { + kind3Follows.flow.value.authors.mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm() + }.flatten().toSet() + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + @OptIn(ExperimentalCoroutinesApi::class) + val flowSet: StateFlow> = + flow.map { relayList -> + relayList.map { it.url }.toSet() + }.onStart { + kind3Follows.flow.value.authors.mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() + }.flatten().toSet() + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt new file mode 100644 index 0000000000..536faa18f7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt @@ -0,0 +1,171 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip02FollowLists + +import android.util.Log +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.UserState +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent +import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag +import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId +import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch + +class FollowListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + // fun getEphemeralChatListAddress() = cache.getOrCreateUser(signer.pubKey) + + fun getFollowListUser(): User = cache.getOrCreateUser(signer.pubKey) + + fun getFollowListFlow(): StateFlow = getFollowListUser().flow().follows.stateFlow + + fun getFollowListEvent(): ContactListEvent? = getFollowListUser().latestContactList + + @OptIn(ExperimentalCoroutinesApi::class) + private val innerFlow: Flow = + getFollowListFlow().transformLatest { + emit(buildKind3Follows(it.user.latestContactList)) + } + + val flow = + innerFlow + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + buildKind3Follows(getFollowListEvent() ?: settings.backupContactList), + ) + + /** + This contains a big OR of everything the user wants to see in the a single feed. + */ + @Immutable + class Kind3Follows( + val authors: Set = emptySet(), + val authorsPlusMe: Set, + val hashtags: Set = emptySet(), + val geotags: Set = emptySet(), + val communities: Set = emptySet(), + ) { + val geotagScopes: Set = geotags.mapTo(mutableSetOf()) { GeohashId.toScope(it) } + val hashtagScopes: Set = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) } + } + + fun buildKind3Follows(latestContactList: ContactListEvent?): Kind3Follows { + // makes sure the output include only valid p tags + val verifiedFollowingUsers = latestContactList?.verifiedFollowKeySet() ?: emptySet() + + return Kind3Follows( + authors = verifiedFollowingUsers, + authorsPlusMe = verifiedFollowingUsers + signer.pubKey, + hashtags = + latestContactList + ?.unverifiedFollowTagSet() + ?.map { it.lowercase() } + ?.toSet() ?: emptySet(), + geotags = + latestContactList + ?.geohashes() + ?.toSet() ?: emptySet(), + communities = + latestContactList + ?.verifiedFollowAddressSet() + ?.toSet() ?: emptySet(), + ) + } + + fun follow( + user: User, + onDone: (ContactListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val contactList = getFollowListEvent() + + if (contactList != null) { + ContactListEvent.followUser(contactList, user.pubkeyHex, signer, onReady = onDone) + } else { + ContactListEvent.createFromScratch( + followUsers = listOf(ContactTag(user.pubkeyHex, user.bestRelayHint(), null)), + followTags = emptyList(), + followGeohashes = emptyList(), + followCommunities = emptyList(), + relayUse = emptyMap(), + signer = signer, + onReady = onDone, + ) + } + } + + fun unfollow( + user: User, + onDone: (ContactListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val contactList = getFollowListEvent() + + if (contactList != null && contactList.tags.isNotEmpty()) { + ContactListEvent.unfollowUser( + contactList, + user.pubkeyHex, + signer, + onReady = onDone, + ) + } + } + + init { + settings.backupContactList?.let { + Log.d("AccountRegisterObservers", "Loading saved contacts ${it.toJson()}") + + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + } + + // saves contact list for the next time. + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Kind 3 Collector Start") + getFollowListFlow().collect { + Log.d("AccountRegisterObservers", "Updating Kind 3 ${signer.pubKey}") + settings.updateContactListTo(it.user.latestContactList) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt new file mode 100644 index 0000000000..89207b4ff8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip02FollowLists + +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.utils.mapOfSet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlin.collections.map + +class FollowsPerOutboxRelay( + kind3Follows: FollowListState, + val cache: LocalCache, + scope: CoroutineScope, +) { + fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.createAddress(pubkey) + + fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey)) + + fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow + + fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent + + fun allRelayListFlows(followList: Set): List> = followList.map { getNIP65RelayListFlow(it) } + + fun combineAllFlows(flows: List>): Flow>> = + combine(flows) { relayListNotes: Array -> + mapOfSet { + relayListNotes.forEach { noteState -> + noteState.note.author?.pubkeyHex?.let { authorHex -> + (noteState.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.forEach { relay -> + add(relay, authorHex) + } + } + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + val flow: StateFlow>> = + kind3Follows.flow.transformLatest { followList -> + val flows: List> = allRelayListFlows(followList.authors) + val relayListFlows = combineAllFlows(flows) + emitAll(relayListFlows) + }.onStart { + emit( + mapOfSet { + kind3Follows.flow.value.authors.map { authorHex -> + getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay -> + add(relay, authorHex) + } + } + }, + ) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyMap(), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt new file mode 100644 index 0000000000..d1e9def879 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmInboxRelayState.kt @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip17Dms + +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn + +class DmInboxRelayState( + // main relay + dmRelayList: DmRelayListState, + // backup relays + nip65RelayList: Nip65RelayListState, + privateOutbox: PrivateStorageRelayListState, + localRelayList: LocalRelayListState, + scope: CoroutineScope, +) { + val flow = + combine( + nip65RelayList.inboxFlow, + dmRelayList.flow, + privateOutbox.flow, + localRelayList.flow, + ) { nip65Inbox, dmRelayList, privateOutBox, localRelays -> + nip65Inbox + dmRelayList + privateOutBox + localRelays + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + nip65RelayList.inboxFlow.value + + dmRelayList.flow.value + + privateOutbox.flow.value + + localRelayList.flow.value, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt new file mode 100644 index 0000000000..711ea3a933 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip17Dms + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class DmRelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getDMRelayListAddress() = ChatMessageRelayListEvent.createAddress(signer.pubKey) + + fun getDMRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getDMRelayListAddress()) + + fun getDMRelayListFlow(): StateFlow = getDMRelayListNote().flow().metadata.stateFlow + + fun getDMRelayList(): ChatMessageRelayListEvent? = getDMRelayListNote().event as? ChatMessageRelayListEvent + + fun normalizeDMRelayListWithBackup(note: Note): Set { + val event = note.event as? ChatMessageRelayListEvent ?: settings.backupDMRelayList + return event?.relays()?.toSet() ?: emptySet() + } + + val flow = + getDMRelayListFlow() + .map { normalizeDMRelayListWithBackup(it.note) } + .onStart { emit(normalizeDMRelayListWithBackup(getDMRelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + fun saveRelayList( + dmRelays: List, + onDone: (ChatMessageRelayListEvent) -> Unit, + ) { + val relayListForDMs = getDMRelayList() + if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) { + ChatMessageRelayListEvent.updateRelayList( + earlierVersion = relayListForDMs, + relays = dmRelays, + signer = signer, + onReady = onDone, + ) + } else { + ChatMessageRelayListEvent.createFromScratch( + relays = dmRelays, + signer = signer, + onReady = onDone, + ) + } + } + + init { + settings.backupDMRelayList?.let { + Log.d("AccountRegisterObservers", "Loading saved DM Relay List ${it.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + LocalCache.justConsumeMyOwnEvent(it) + } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "NIP-17 Relay List Collector Start") + getDMRelayListFlow().collect { + Log.d("AccountRegisterObservers", "Updating DM Relay List for ${signer.pubKey}") + (it.note.event as? ChatMessageRelayListEvent)?.let { + settings.updateDMRelayList(it) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt new file mode 100644 index 0000000000..54271181b2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip18Reposts + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent + +class RepostAction { + companion object { + fun repost( + note: Note, + signer: NostrSigner, + onDone: (Event) -> Unit, + ) { + if (!signer.isWriteable()) return + val noteEvent = note.event ?: return + + if (note.hasBoostedInTheLast5Minutes(signer.pubKey)) { + // has already bosted in the past 5mins + return + } + + val noteHint = note.relayHintUrl() + val authorHint = note.author?.bestRelayHint() + + val template = + if (noteEvent.kind == 1) { + RepostEvent.build(noteEvent, noteHint, authorHint) + } else { + GenericRepostEvent.build(noteEvent, noteHint, authorHint) + } + + signer.sign(template, onDone) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt new file mode 100644 index 0000000000..9174c13d3d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip25Reactions + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip17Dm.NIP17Factory +import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag + +class ReactionAction { + companion object { + suspend fun reactTo( + note: Note, + reaction: String, + by: User, + signer: NostrSigner, + onPublic: (ReactionEvent) -> Unit, + onPrivate: (NIP17Factory.Result) -> Unit, + ) { + if (!signer.isWriteable()) return + + if (note.hasReacted(by, reaction)) { + // has already liked this note + return + } + + val noteEvent = note.event + if (noteEvent is NIP17Group) { + val users = noteEvent.groupMembers().toList() + + if (reaction.startsWith(":")) { + val emojiUrl = EmojiUrlTag.decode(reaction) + if (emojiUrl != null) { + note.toEventHint()?.let { + NIP17Factory().createReactionWithinGroup( + emojiUrl = emojiUrl, + originalNote = it, + to = users, + signer = signer, + ) { + onPrivate(it) + } + } + + return + } + } + + note.toEventHint()?.let { + NIP17Factory().createReactionWithinGroup( + content = reaction, + originalNote = it, + to = users, + signer = signer, + ) { + onPrivate(it) + } + } + return + } else { + if (reaction.startsWith(":")) { + val emojiUrl = EmojiUrlTag.decode(reaction) + if (emojiUrl != null) { + note.event?.let { + val template = ReactionEvent.build(emojiUrl, EventHintBundle(it, note.relayHintUrl())) + + signer.sign( + template, + onReady = onPublic, + ) + } + + return + } + } + + note.toEventHint()?.let { + signer.sign( + ReactionEvent.build(reaction, it), + onReady = onPublic, + ) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt index 64368a690b..5dd1f9cd9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt @@ -20,18 +20,23 @@ */ package com.vitorpamplona.amethyst.model.nip28PublicChats +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.tryAndWait import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -39,12 +44,14 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch import kotlin.coroutines.resume class PublicChatListState( val signer: NostrSigner, val cache: LocalCache, val scope: CoroutineScope, + val settings: AccountSettings, ) { fun getChannelListAddress() = ChannelListEvent.createAddress(signer.pubKey) @@ -54,31 +61,27 @@ class PublicChatListState( fun getChannelList(): ChannelListEvent? = getChannelListNote().event as? ChannelListEvent + suspend fun publicChatListWithBackup(note: Note): Set { + return publicChatList( + note.event as? ChannelListEvent ?: settings.backupChannelList, + ) + } + + suspend fun publicChatList(event: ChannelListEvent?): Set { + return tryAndWait { continuation -> + event?.publicAndPrivateChannels(signer) { + continuation.resume(it) + } + } ?: emptySet() + } + @OptIn(ExperimentalCoroutinesApi::class) - val livePublicChatList: StateFlow> by lazy { + val flow: StateFlow> by lazy { getChannelListFlow() .transformLatest { noteState -> - val set = - tryAndWait { continuation -> - (noteState.note.event as? ChannelListEvent)?.publicAndPrivateChannels(signer) { - continuation.resume(it) - } - } - - if (set != null) { - emit(set) - } + emit(publicChatListWithBackup(noteState.note)) }.onStart { - val set = - tryAndWait { continuation -> - getChannelList()?.publicAndPrivateChannels(signer) { - continuation.resume(it) - } - } - - if (set != null) { - emit(set) - } + emit(publicChatListWithBackup(getChannelListNote())) }.flowOn(Dispatchers.Default) .stateIn( scope, @@ -88,8 +91,8 @@ class PublicChatListState( } @OptIn(ExperimentalCoroutinesApi::class) - val livePublicChatEventIdSet: StateFlow> by lazy { - livePublicChatList + val flowSet: StateFlow> by lazy { + flow .map { it.mapTo(mutableSetOf()) { it.eventId } }.flowOn(Dispatchers.Default) @@ -104,6 +107,7 @@ class PublicChatListState( channel: PublicChatChannel, onDone: (ChannelListEvent) -> Unit, ) { + if (!signer.isWriteable()) return val publicChatList = getChannelList() val fullHint = channel.toEventHint() @@ -127,6 +131,7 @@ class PublicChatListState( channels: List, onDone: (ChannelListEvent) -> Unit, ) { + if (!signer.isWriteable()) return val publicChatList = getChannelList() val partialHint = channels.map { it.toEventId() } @@ -141,10 +146,33 @@ class PublicChatListState( channel: PublicChatChannel, onDone: (ChannelListEvent) -> Unit, ) { + if (!signer.isWriteable()) return val publicChatList = getChannelList() if (publicChatList != null) { ChannelListEvent.removeChannel(publicChatList, channel.idHex, signer, onReady = onDone) } } + + init { + settings.backupChannelList?.let { event -> + Log.d("AccountRegisterObservers", "Loading saved channel list ${event.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + event.privateTags(signer) { + LocalCache.justConsumeMyOwnEvent(event) + } + } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Channel List Collector Start") + getChannelListFlow().collect { + Log.d("AccountRegisterObservers", "Channel List for ${signer.pubKey}") + (it.note.event as? ChannelListEvent)?.let { + settings.updateChannelListTo(it) + } + } + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt index 4c98580122..9d43b89ec8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip30CustomEmojis import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses @@ -67,7 +68,7 @@ class EmojiPackState( } @OptIn(ExperimentalCoroutinesApi::class) - val liveEmojiSelectionPack: StateFlow>?> by lazy { + val flow: StateFlow>?> by lazy { getEmojiPackSelectionFlow() .transformLatest { emit(convertEmojiSelectionPack(it.note.event as? EmojiPackSelectionEvent)) @@ -98,7 +99,7 @@ class EmojiPackState( @OptIn(ExperimentalCoroutinesApi::class) val myEmojis by lazy { - liveEmojiSelectionPack + flow .transformLatest { emojiList -> if (emojiList != null) { emitAll( @@ -116,4 +117,40 @@ class EmojiPackState( mergePack(convertEmojiSelectionPack(getEmojiPackSelection())?.map { it.value }?.toTypedArray() ?: emptyArray()), ) } + + fun addEmojiPack( + emojiPack: Note, + onDone: (EmojiPackSelectionEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + + val emojiPackEvent = emojiPack.event + if (emojiPackEvent !is EmojiPackEvent) return + + val eventHint = emojiPack.toEventHint() ?: return + + val usersEmojiList = getEmojiPackSelection() + if (usersEmojiList == null) { + val template = EmojiPackSelectionEvent.build(listOf(eventHint)) + signer.sign(template, onDone) + } else { + val template = EmojiPackSelectionEvent.add(usersEmojiList, eventHint) + signer.sign(template, onDone) + } + } + + fun removeEmojiPack( + emojiPack: Note, + onDone: (EmojiPackSelectionEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + + val usersEmojiList = getEmojiPackSelection() ?: return + + val emojiPackEvent = emojiPack.event + if (emojiPackEvent !is EmojiPackEvent) return + + val template = EmojiPackSelectionEvent.remove(usersEmojiList, emojiPackEvent) + signer.sign(template, onDone) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt new file mode 100644 index 0000000000..645d8a2c8b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip38UserStatuses + +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent + +class UserStatusAction { + companion object { + fun create( + newStatus: String, + signer: NostrSigner, + onDone: (StatusEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + + StatusEvent.create(newStatus, "general", expiration = null, signer, onReady = onDone) + } + + fun update( + oldStatus: AddressableNote, + newStatus: String, + signer: NostrSigner, + onDone: (StatusEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val oldEvent = oldStatus.event as? StatusEvent ?: return + + StatusEvent.update(oldEvent, newStatus, signer, onReady = onDone) + } + + fun delete( + oldStatus: AddressableNote, + signer: NostrSigner, + onDone: (Event) -> Unit, + ) { + if (!signer.isWriteable()) return + val oldEvent = oldStatus.event as? StatusEvent ?: return + + StatusEvent.clear(oldEvent, signer) { event -> + onDone(event) + + signer.sign( + DeletionEvent.buildForVersionOnly(listOf(event)), + onReady = onDone, + ) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt new file mode 100644 index 0000000000..5f4a751061 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip50Search + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.DefaultSearchRelayList +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class SearchRelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getSearchRelayListAddress() = SearchRelayListEvent.createAddress(signer.pubKey) + + fun getSearchRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getSearchRelayListAddress()) + + fun getSearchRelayListFlow(): StateFlow = getSearchRelayListNote().flow().metadata.stateFlow + + fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent + + fun normalizeSearchRelayListWithBackup(note: Note): Set { + val event = note.event as? SearchRelayListEvent ?: settings.backupSearchRelayList + return event?.relays()?.toSet() ?: DefaultSearchRelayList + } + + val flow = + getSearchRelayListFlow() + .map { normalizeSearchRelayListWithBackup(it.note) } + .onStart { emit(normalizeSearchRelayListWithBackup(getSearchRelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + fun saveRelayList( + searchRelays: List, + onDone: (SearchRelayListEvent) -> Unit, + ) { + val relayListForSearch = getSearchRelayList() + + if (relayListForSearch != null && relayListForSearch.tags.isNotEmpty()) { + SearchRelayListEvent.updateRelayList( + earlierVersion = relayListForSearch, + relays = searchRelays, + signer = signer, + onReady = onDone, + ) + } else { + SearchRelayListEvent.createFromScratch( + relays = searchRelays, + signer = signer, + onReady = onDone, + ) + } + } + + init { + settings.backupSearchRelayList?.let { + Log.d("AccountRegisterObservers", "Loading saved search relay list ${it.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Search Relay List Collector Start") + getSearchRelayListFlow().collect { + Log.d("AccountRegisterObservers", "Updating Search Relay List for ${signer.pubKey}") + (it.note.event as? SearchRelayListEvent)?.let { + settings.updateSearchRelayList(it) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt new file mode 100644 index 0000000000..148cec26b0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists + +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlin.coroutines.resume + +class BlockPeopleListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, +) { + fun getBlockListAddress() = PeopleListEvent.createBlockAddress(signer.pubKey) + + fun getBlockListNote() = LocalCache.getOrCreateAddressableNote(getBlockListAddress()) + + fun getBlockListFlow(): StateFlow = getBlockListNote().flow().metadata.stateFlow + + fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent + + suspend fun blockListWithBackup(note: Note): PeopleListEvent.UsersAndWords { + return blockList( + note.event as? PeopleListEvent, + ) + } + + suspend fun blockList(event: PeopleListEvent?): PeopleListEvent.UsersAndWords { + return tryAndWait { continuation -> + event?.publicAndPrivateUsersAndWords(signer) { + continuation.resume(it) + } + } ?: PeopleListEvent.UsersAndWords() + } + + val flow = + getBlockListFlow() + .map { blockListWithBackup(it.note) } + .onStart { emit(blockListWithBackup(getBlockListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + PeopleListEvent.UsersAndWords(), + ) + + fun hideUser( + pubkeyHex: String, + onDone: (PeopleListEvent) -> Unit, + ) { + val blockList = getBlockList() + + if (blockList != null) { + PeopleListEvent.addUser( + earlierVersion = blockList, + pubKeyHex = pubkeyHex, + isPrivate = true, + signer = signer, + onReady = onDone, + ) + } else { + PeopleListEvent.createListWithUser( + name = PeopleListEvent.BLOCK_LIST_D_TAG, + pubKeyHex = pubkeyHex, + isPrivate = true, + signer = signer, + onReady = onDone, + ) + } + } + + fun showUser( + pubkeyHex: String, + onDone: (PeopleListEvent) -> Unit, + ) { + val blockList = getBlockList() + + if (blockList != null) { + PeopleListEvent.removeUser( + earlierVersion = blockList, + pubKeyHex = pubkeyHex, + signer = signer, + onReady = onDone, + ) + } + } + + fun hideWord( + word: String, + onDone: (PeopleListEvent) -> Unit, + ) { + val blockList = getBlockList() + + if (blockList != null) { + PeopleListEvent.addWord( + earlierVersion = blockList, + word = word, + isPrivate = true, + signer = signer, + onReady = onDone, + ) + } else { + PeopleListEvent.createListWithWord( + name = PeopleListEvent.BLOCK_LIST_D_TAG, + word = word, + isPrivate = true, + signer = signer, + onReady = onDone, + ) + } + } + + fun showWord( + word: String, + onDone: (PeopleListEvent) -> Unit, + ) { + val blockList = getBlockList() + + if (blockList != null) { + PeopleListEvent.removeWord( + earlierVersion = blockList, + word = word, + signer = signer, + onReady = onDone, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt new file mode 100644 index 0000000000..3464486e4b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch +import kotlin.coroutines.resume + +class GeohashListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getGeohashListAddress() = GeohashListEvent.createAddress(signer.pubKey) + + fun getGeohashListNote(): AddressableNote = cache.getOrCreateAddressableNote(getGeohashListAddress()) + + fun getGeohashListFlow(): StateFlow = getGeohashListNote().flow().metadata.stateFlow + + fun getGeohashList(): GeohashListEvent? = getGeohashListNote().event as? GeohashListEvent + + suspend fun geohashListWithBackup(note: Note): Set { + return geohashList( + note.event as? GeohashListEvent ?: settings.backupGeohashList, + ) + } + + suspend fun geohashList(event: GeohashListEvent?): Set { + return tryAndWait { continuation -> + event?.publicAndPrivateGeohash(signer) { + continuation.resume(it) + } + } ?: emptySet() + } + + @OptIn(ExperimentalCoroutinesApi::class) + val flow: StateFlow> by lazy { + getGeohashListFlow() + .transformLatest { noteState -> + emit(geohashListWithBackup(noteState.note)) + }.onStart { + emit(geohashListWithBackup(getGeohashListNote())) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + } + + fun follow( + geohashs: List, + onDone: (GeohashListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val geohashList = getGeohashList() + + if (geohashList == null) { + GeohashListEvent.createGeohashs(geohashs, true, signer, onReady = onDone) + } else { + GeohashListEvent.addGeohashs(geohashList, geohashs, true, signer, onReady = onDone) + } + } + + fun follow( + geohash: String, + onDone: (GeohashListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val geohashList = getGeohashList() + + if (geohashList == null) { + GeohashListEvent.createGeohash(geohash, true, signer, onReady = onDone) + } else { + GeohashListEvent.addGeohash(geohashList, geohash, true, signer, onReady = onDone) + } + } + + fun unfollow( + geohash: String, + onDone: (GeohashListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val geohashList = getGeohashList() + + if (geohashList != null) { + GeohashListEvent.removeGeohash(geohashList, geohash, signer, onReady = onDone) + } + } + + init { + settings.backupGeohashList?.let { event -> + Log.d("AccountRegisterObservers", "Loading saved Geohash list ${event.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + event.privateTags(signer) { + LocalCache.justConsumeMyOwnEvent(event) + } + } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Geohash List Collector Start") + getGeohashListFlow().collect { + Log.d("AccountRegisterObservers", "Geohash List for ${signer.pubKey}") + (it.note.event as? GeohashListEvent)?.let { + settings.updateGeohashListTo(it) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt new file mode 100644 index 0000000000..65d18040e0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch +import kotlin.coroutines.resume + +class HashtagListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getHashtagListAddress() = HashtagListEvent.createAddress(signer.pubKey) + + fun getHashtagListNote(): AddressableNote = cache.getOrCreateAddressableNote(getHashtagListAddress()) + + fun getHashtagListFlow(): StateFlow = getHashtagListNote().flow().metadata.stateFlow + + fun getHashtagList(): HashtagListEvent? = getHashtagListNote().event as? HashtagListEvent + + suspend fun hashtagListWithBackup(note: Note): Set { + return hashtagList( + note.event as? HashtagListEvent ?: settings.backupHashtagList, + ) + } + + suspend fun hashtagList(event: HashtagListEvent?): Set { + return tryAndWait { continuation -> + event?.publicAndPrivateHashtag(signer) { + continuation.resume(it) + } + } ?: emptySet() + } + + @OptIn(ExperimentalCoroutinesApi::class) + val flow: StateFlow> by lazy { + getHashtagListFlow() + .transformLatest { noteState -> + emit(hashtagListWithBackup(noteState.note)) + }.onStart { + emit(hashtagListWithBackup(getHashtagListNote())) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + } + + fun follow( + hashtags: List, + onDone: (HashtagListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val hashtagList = getHashtagList() + + if (hashtagList == null) { + HashtagListEvent.createHashtags(hashtags, true, signer, onReady = onDone) + } else { + HashtagListEvent.addHashtags(hashtagList, hashtags, true, signer, onReady = onDone) + } + } + + fun follow( + hashtag: String, + onDone: (HashtagListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val hashtagList = getHashtagList() + + if (hashtagList == null) { + HashtagListEvent.createHashtag(hashtag, true, signer, onReady = onDone) + } else { + HashtagListEvent.addHashtag(hashtagList, hashtag, true, signer, onReady = onDone) + } + } + + fun unfollow( + hashtag: String, + onDone: (HashtagListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val hashtagList = getHashtagList() + + if (hashtagList != null) { + HashtagListEvent.removeHashtag(hashtagList, hashtag, signer, onReady = onDone) + } + } + + init { + settings.backupHashtagList?.let { event -> + Log.d("AccountRegisterObservers", "Loading saved Hashtag list ${event.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + event.privateTags(signer) { + LocalCache.justConsumeMyOwnEvent(event) + } + } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Hashtag List Collector Start") + getHashtagListFlow().collect { + Log.d("AccountRegisterObservers", "Hashtag List for ${signer.pubKey}") + (it.note.event as? HashtagListEvent)?.let { + settings.updateHashtagListTo(it) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt new file mode 100644 index 0000000000..8a953176f1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.utils.DualCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.runBlocking + +class HiddenUsersState( + val muteList: StateFlow, + val blockList: StateFlow, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + var transientHiddenUsers: MutableStateFlow> = MutableStateFlow(setOf()) + + @Immutable + class LiveHiddenUsers( + val hiddenUsers: Set, + val spammers: Set, + val hiddenWords: Set, + val showSensitiveContent: Boolean?, + ) { + // speeds up isHidden calculations + val hiddenUsersHashCodes = hiddenUsers.mapTo(HashSet()) { it.hashCode() } + val spammersHashCodes = spammers.mapTo(HashSet()) { it.hashCode() } + val hiddenWordsCase = hiddenWords.map { DualCase(it.lowercase(), it.uppercase()) } + + fun isUserHidden(userHex: HexKey) = hiddenUsers.contains(userHex) || spammers.contains(userHex) + } + + suspend fun assembleLiveHiddenUsers( + blockList: PeopleListEvent.UsersAndWords, + muteList: PeopleListEvent.UsersAndWords, + transientHiddenUsers: Set, + showSensitiveContent: Boolean?, + ): LiveHiddenUsers { + return LiveHiddenUsers( + hiddenUsers = blockList.users + muteList.users, + hiddenWords = blockList.words + muteList.words, + spammers = transientHiddenUsers, + showSensitiveContent = showSensitiveContent, + ) + } + + val flow: StateFlow by lazy { + combineTransform( + blockList, + muteList, + transientHiddenUsers, + settings.syncedSettings.security.showSensitiveContent, + ) { blockList, muteList, transientHiddenUsers, showSensitiveContent -> + checkNotInMainThread() + emit(assembleLiveHiddenUsers(blockList, muteList, transientHiddenUsers, showSensitiveContent)) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + runBlocking { + assembleLiveHiddenUsers( + blockList.value, + muteList.value, + transientHiddenUsers.value, + settings.syncedSettings.security.showSensitiveContent.value, + ) + }, + ) + } + + fun resetTransientUsers() { + transientHiddenUsers.update { + emptySet() + } + } + + fun showUser(pubkeyHex: HexKey) { + transientHiddenUsers.update { it - pubkeyHex } + } + + fun hideUser(pubkeyHex: HexKey) { + transientHiddenUsers.update { it + pubkeyHex } + } + + fun isHidden(pubkeyHex: HexKey) = pubkeyHex in transientHiddenUsers.value +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt new file mode 100644 index 0000000000..42e68cda70 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlin.coroutines.resume + +class MuteListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getMuteListAddress() = MuteListEvent.createAddress(signer.pubKey) + + fun getMuteListNote() = cache.getOrCreateAddressableNote(getMuteListAddress()) + + fun getMuteListFlow(): StateFlow = getMuteListNote().flow().metadata.stateFlow + + fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent + + suspend fun muteListWithBackup(note: Note): PeopleListEvent.UsersAndWords { + return muteList( + note.event as? MuteListEvent ?: settings.backupMuteList, + ) + } + + suspend fun muteList(event: MuteListEvent?): PeopleListEvent.UsersAndWords { + return tryAndWait { continuation -> + event?.publicAndPrivateUsersAndWords(signer) { + continuation.resume(it) + } + } ?: PeopleListEvent.UsersAndWords() + } + + val flow = + getMuteListFlow() + .map { muteListWithBackup(it.note) } + .onStart { emit(muteListWithBackup(getMuteListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + PeopleListEvent.UsersAndWords(), + ) + + fun hideUser( + pubkeyHex: String, + onDone: (MuteListEvent) -> Unit, + ) { + val muteList = getMuteList() + + if (muteList != null) { + MuteListEvent.addUser( + earlierVersion = muteList, + pubKeyHex = pubkeyHex, + isPrivate = true, + signer = signer, + onReady = onDone, + ) + } else { + MuteListEvent.createListWithUser( + pubKeyHex = pubkeyHex, + isPrivate = true, + signer = signer, + onReady = onDone, + ) + } + } + + fun showUser( + pubkeyHex: String, + onDone: (MuteListEvent) -> Unit, + ) { + val muteList = getMuteList() + + if (muteList != null) { + MuteListEvent.removeUser( + earlierVersion = muteList, + pubKeyHex = pubkeyHex, + signer = signer, + onReady = onDone, + ) + } + } + + fun hideWord( + word: String, + onDone: (MuteListEvent) -> Unit, + ) { + val muteList = getMuteList() + + if (muteList != null) { + MuteListEvent.addWord( + earlierVersion = muteList, + word = word, + isPrivate = true, + signer = signer, + onReady = onDone, + ) + } else { + MuteListEvent.createListWithWord( + word = word, + isPrivate = true, + signer = signer, + onReady = onDone, + ) + } + } + + fun showWord( + word: String, + onDone: (MuteListEvent) -> Unit, + ) { + val muteList = getMuteList() + + if (muteList != null) { + MuteListEvent.removeWord( + earlierVersion = muteList, + word = word, + signer = signer, + onReady = onDone, + ) + } + } + + init { + settings.backupMuteList?.let { event -> + Log.d("AccountRegisterObservers", "Loading saved mute list ${event.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + event.privateTags(signer) { + LocalCache.justConsumeMyOwnEvent(event) + } + } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Mute List Collector Start") + getMuteListFlow().collect { + Log.d("AccountRegisterObservers", "Updating Mute List for ${signer.pubKey}") + (it.note.event as? MuteListEvent)?.let { + settings.updateMuteList(it) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt new file mode 100644 index 0000000000..bdec0ec8cf --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip56Reports + +import android.R.attr.type +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip56Reports.ReportEvent +import com.vitorpamplona.quartz.nip56Reports.ReportType + +class ReportAction { + companion object { + fun report( + user: User, + type: ReportType, + by: User, + signer: NostrSigner, + onDone: (ReportEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + + if (user.hasReport(by, type)) { + // has already reported this note + return + } + + val template = ReportEvent.build(user.pubkeyHex, type) + + signer.sign(template, onDone) + } + + suspend fun report( + note: Note, + type: ReportType, + content: String = "", + by: User, + signer: NostrSigner, + onDone: (ReportEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + + if (note.hasReport(by, type)) { + // has already reported this note + return + } + + note.event?.let { + signer.sign(ReportEvent.build(it, type), onDone) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt new file mode 100644 index 0000000000..1018b03aec --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip65RelayList + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.Constants +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class Nip65RelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getNIP65RelayListAddress() = AdvertisedRelayListEvent.createAddress(signer.pubKey) + + fun getNIP65RelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress()) + + fun getNIP65RelayListFlow(): StateFlow = getNIP65RelayListNote().flow().metadata.stateFlow + + fun getNIP65RelayList(): AdvertisedRelayListEvent? = getNIP65RelayListNote().event as? AdvertisedRelayListEvent + + fun normalizeNIP65WriteRelayListWithBackup(note: Note): Set { + val event = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList + return event?.writeRelaysNorm()?.toSet() ?: Constants.eventFinderRelays + } + + fun normalizeNIP65ReadRelayListWithBackup(note: Note): Set { + val event = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList + return event?.readRelaysNorm()?.toSet() ?: Constants.eventFinderRelays + } + + fun normalizeNIP65AllRelayListWithBackup(note: Note): Set { + val event = note.event as? AdvertisedRelayListEvent ?: settings.backupNIP65RelayList + return event?.relays()?.map { it.relayUrl }?.toSet() ?: Constants.eventFinderRelays + } + + val outboxFlow = + getNIP65RelayListFlow() + .map { normalizeNIP65WriteRelayListWithBackup(it.note) } + .onStart { emit(normalizeNIP65ReadRelayListWithBackup(getNIP65RelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + val inboxFlow = + getNIP65RelayListFlow() + .map { normalizeNIP65ReadRelayListWithBackup(it.note) } + .onStart { emit(normalizeNIP65ReadRelayListWithBackup(getNIP65RelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + val allFlow = + getNIP65RelayListFlow() + .map { normalizeNIP65AllRelayListWithBackup(it.note) } + .onStart { emit(normalizeNIP65AllRelayListWithBackup(getNIP65RelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + fun saveRelayList( + relays: List, + onDone: (AdvertisedRelayListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + + val nip65RelayList = getNIP65RelayList() + + if (nip65RelayList != null) { + AdvertisedRelayListEvent.replaceRelayListWith( + earlierVersion = nip65RelayList, + newRelays = relays, + signer = signer, + onReady = onDone, + ) + } else { + AdvertisedRelayListEvent.createFromScratch( + relays = relays, + signer = signer, + onReady = onDone, + ) + } + } + + init { + settings.backupNIP65RelayList?.let { + Log.d("AccountRegisterObservers", "Loading saved nip65 relay list ${it.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "NIP-65 Relay List Collector Start") + getNIP65RelayListFlow().collect { + Log.d("AccountRegisterObservers", "Updating NIP-65 List for ${signer.pubKey}") + (it.note.event as? AdvertisedRelayListEvent)?.let { + settings.updateNIP65RelayList(it) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt new file mode 100644 index 0000000000..6467b4c7bc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip65RelayList + +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest + +class OutboxRelaySetState( + usersToLoad: MutableStateFlow>, + val cache: LocalCache, + scope: CoroutineScope, +) { + fun getNIP65RelayListAddress(pubkey: HexKey) = AdvertisedRelayListEvent.Companion.createAddress(pubkey) + + fun getNIP65RelayListNote(pubkey: HexKey): AddressableNote = cache.getOrCreateAddressableNote(getNIP65RelayListAddress(pubkey)) + + fun getNIP65RelayListFlow(pubkey: HexKey): StateFlow = getNIP65RelayListNote(pubkey).flow().metadata.stateFlow + + fun getNIP65RelayList(pubkey: HexKey): AdvertisedRelayListEvent? = getNIP65RelayListNote(pubkey).event as? AdvertisedRelayListEvent + + fun allRelayListFlows(followList: Set): List> = followList.map { getNIP65RelayListFlow(it) } + + fun combineAllFlows(flows: List>): Flow> = + combine(flows) { relayListNotes: Array -> + relayListNotes.mapNotNull { + (it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() + } + }.map { + it.flatten().toSet() + } + + @OptIn(ExperimentalCoroutinesApi::class) + val flow: StateFlow> = + usersToLoad.transformLatest { followList -> + val flows: List> = allRelayListFlows(followList) + val relayListFlows = combineAllFlows(flows) + emitAll(relayListFlows) + }.onStart { + emit( + usersToLoad.value.mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm() + }.flatten().toSet(), + ) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Companion.Eagerly, + emptySet(), + ) + + @OptIn(ExperimentalCoroutinesApi::class) + val flowSet: StateFlow> = + flow.map { relayList -> + relayList.map { it.url }.toSet() + }.onStart { + emit( + usersToLoad.value.mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() + }.flatten().toSet(), + ) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Companion.Eagerly, + emptySet(), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt new file mode 100644 index 0000000000..375c88034a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip72Communities + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.launch +import kotlin.coroutines.resume + +class CommunityListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getCommunityListAddress() = CommunityListEvent.createAddress(signer.pubKey) + + fun getCommunityListNote(): AddressableNote = cache.getOrCreateAddressableNote(getCommunityListAddress()) + + fun getCommunityListFlow(): StateFlow = getCommunityListNote().flow().metadata.stateFlow + + fun getCommunityList(): CommunityListEvent? = getCommunityListNote().event as? CommunityListEvent + + suspend fun communityListWithBackup(note: Note): Set { + return communityList( + note.event as? CommunityListEvent ?: settings.backupCommunityList, + ) + } + + suspend fun communityList(event: CommunityListEvent?): Set { + return tryAndWait { continuation -> + event?.publicAndPrivateCommunities(signer) { + continuation.resume(it) + } + } ?: emptySet() + } + + @OptIn(ExperimentalCoroutinesApi::class) + val flow: StateFlow> by lazy { + getCommunityListFlow() + .transformLatest { noteState -> + emit(communityListWithBackup(noteState.note)) + }.onStart { + emit(communityListWithBackup(getCommunityListNote())) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + val flowSet: StateFlow> by lazy { + flow + .map { + it.mapTo(mutableSetOf()) { it.addressId } + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + } + + fun follow( + communities: List, + onDone: (CommunityListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val communityList = getCommunityList() + + val partialHint = communities.mapNotNull { it.toEventHint() } + if (communityList == null) { + CommunityListEvent.createCommunities(partialHint, true, signer, onReady = onDone) + } else { + CommunityListEvent.addCommunities(communityList, partialHint, true, signer, onReady = onDone) + } + } + + fun follow( + community: AddressableNote, + onDone: (CommunityListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val communityList = getCommunityList() + + val fullHint = community.toEventHint() + if (fullHint != null) { + if (communityList == null) { + CommunityListEvent.createCommunity(fullHint, true, signer, onReady = onDone) + } else { + CommunityListEvent.addCommunity(communityList, fullHint, true, signer, onReady = onDone) + } + } else { + val partialHint = community.toATag() + if (communityList == null) { + CommunityListEvent.createCommunity(partialHint, true, signer, onReady = onDone) + } else { + CommunityListEvent.addCommunity(communityList, partialHint, true, signer, onReady = onDone) + } + } + } + + fun unfollow( + community: AddressableNote, + onDone: (CommunityListEvent) -> Unit, + ) { + if (!signer.isWriteable()) return + val communityList = getCommunityList() + + if (communityList != null) { + CommunityListEvent.removeCommunity(communityList, community.address.toValue(), signer, onReady = onDone) + } + } + + init { + settings.backupCommunityList?.let { event -> + Log.d("AccountRegisterObservers", "Loading saved Community list ${event.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + event.privateTags(signer) { + LocalCache.justConsumeMyOwnEvent(event) + } + } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Community List Collector Start") + getCommunityListFlow().collect { + Log.d("AccountRegisterObservers", "Community List for ${signer.pubKey}") + (it.note.event as? CommunityListEvent)?.let { + settings.updateCommunityListTo(it) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt new file mode 100644 index 0000000000..da6285e74f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip78AppSpecific + +import android.util.Log +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +class AppSpecificState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + companion object { + const val APP_SPECIFIC_DATA_D_TAG = "AmethystSettings" + } + + fun getAppSpecificDataAddress() = AppSpecificDataEvent.createAddress(signer.pubKey, APP_SPECIFIC_DATA_D_TAG) + + fun getAppSpecificDataNote() = cache.getOrCreateAddressableNote(getAppSpecificDataAddress()) + + fun getAppSpecificDataFlow(): StateFlow = getAppSpecificDataNote().flow().metadata.stateFlow + + fun saveNewAppSpecificData(onDone: (AppSpecificDataEvent) -> Unit) { + val toInternal = settings.syncedSettings.toInternal() + signer.nip44Encrypt(EventMapper.mapper.writeValueAsString(toInternal), signer.pubKey) { encrypted -> + AppSpecificDataEvent.create( + dTag = APP_SPECIFIC_DATA_D_TAG, + description = encrypted, + otherTags = emptyArray(), + signer = signer, + onReady = onDone, + ) + } + } + + init { + settings.backupAppSpecificData?.let { event -> + Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { + LocalCache.justConsumeMyOwnEvent(event) + signer.decrypt(event.content, event.pubKey) { decrypted -> + try { + val syncedSettings = EventMapper.mapper.readValue(decrypted) + settings.syncedSettings.updateFrom(syncedSettings) + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) + e.printStackTrace() + AccountSyncedSettingsInternal() + } + } + } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "AppSpecificData Collector Start") + getAppSpecificDataFlow().collect { + Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${signer.pubKey}") + (it.note.event as? AppSpecificDataEvent)?.let { + signer.decrypt(it.content, it.pubKey) { decrypted -> + val syncedSettings = + try { + EventMapper.mapper.readValue(decrypted) + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) + e.printStackTrace() + AccountSyncedSettingsInternal() + } + + settings.updateAppSpecificData(it, syncedSettings) + } + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt new file mode 100644 index 0000000000..079e096bf5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip96FileStorage + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class FileStorageServerListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getFileServersAddress() = FileServersEvent.createAddress(signer.pubKey) + + fun getFileServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getFileServersAddress()) + + fun getFileServersListFlow(): StateFlow = getFileServersNote().flow().metadata.stateFlow + + fun getFileServersList(): FileServersEvent? = getFileServersNote().event as? FileServersEvent + + fun normalizeServers(note: Note): List { + val event = note.event as? FileServersEvent + return event?.servers() ?: emptyList() + } + + val fileServers = + getFileServersListFlow() + .map { normalizeServers(it.note) } + .onStart { emit(normalizeServers(getFileServersNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + + fun saveFileServersList( + servers: List, + onDone: (FileServersEvent) -> Unit, + ) { + val serverList = getFileServersList() + + val template = + if (serverList != null && serverList.tags.isNotEmpty()) { + FileServersEvent.replaceServers(serverList, servers) + } else { + FileServersEvent.build(servers) + } + + signer.sign(template, onDone) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt new file mode 100644 index 0000000000..8cf1196f16 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nipB7Blossom + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class BlossomServerListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getBlossomServersAddress() = BlossomServersEvent.createAddress(signer.pubKey) + + fun getBlossomServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlossomServersAddress()) + + fun getBlossomServersListFlow(): StateFlow = getBlossomServersNote().flow().metadata.stateFlow + + fun getBlossomServersList(): BlossomServersEvent? = getBlossomServersNote().event as? BlossomServersEvent + + fun normalizeServers(note: Note): List { + val event = note.event as? FileServersEvent + return event?.servers() ?: emptyList() + } + + val fileServers = + getBlossomServersListFlow() + .map { normalizeServers(it.note) } + .onStart { emit(normalizeServers(getBlossomServersNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + + fun saveBlossomServersList( + servers: List, + onDone: (BlossomServersEvent) -> Unit, + ) { + val serverList = getBlossomServersList() + + if (serverList != null && serverList.tags.isNotEmpty()) { + BlossomServersEvent.updateRelayList( + earlierVersion = serverList, + relays = servers, + signer = signer, + onReady = onDone, + ) + } else { + BlossomServersEvent.createFromScratch( + relays = servers, + signer = signer, + onReady = onDone, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt index 3b142b654b..d7d3831988 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/observables/LatestByKindAndAuthor.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -51,10 +52,8 @@ class LatestByKindAndAuthor( if ((kind in 10000..19999) || (kind in 30000..39999)) { LocalCache.addressables .maxOrNullOf( - filter = { idHex: String, note: AddressableNote -> - note.event?.let { - it.kind == kind && it.pubKey == pubkey - } == true + filter = { address: Address, note: AddressableNote -> + address.kind == kind && address.pubKeyHex == pubkey }, comparator = CreatedAtComparatorAddresses, )?.event as? T diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt new file mode 100644 index 0000000000..663b8adac2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.serverList + +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState +import com.vitorpamplona.amethyst.model.nip51Lists.GeohashListState +import com.vitorpamplona.amethyst.model.nip51Lists.HashtagListState +import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class MergedFollowListsState( + val kind3List: FollowListState, + val hashtagList: HashtagListState, + val geohashList: GeohashListState, + val communityList: CommunityListState, + val scope: CoroutineScope, +) { + fun mergeLists( + kind3: FollowListState.Kind3Follows, + hashtages: Set, + geohashes: Set, + community: Set, + ): FollowListState.Kind3Follows { + return FollowListState.Kind3Follows( + kind3.authors, + kind3.authorsPlusMe, + kind3.hashtags + hashtages, + kind3.geotags + geohashes, + kind3.communities + community.map { it.addressId }, + ) + } + + val flow: StateFlow = + combine( + kind3List.flow, + hashtagList.flow, + geohashList.flow, + communityList.flow, + ) { kind3, hashtag, geohash, community -> + mergeLists(kind3, hashtag, geohash, community) + } + .onStart { + emit( + mergeLists( + kind3List.flow.value, + hashtagList.flow.value, + geohashList.flow.value, + communityList.flow.value, + ), + ) + } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + kind3List.flow.value, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt new file mode 100644 index 0000000000..714c7d23a7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.serverList + +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class MergedFollowPlusMineRelayListsState( + val followsOutboxRelayList: FollowListOutboxRelays, + val nip65RelayList: Nip65RelayListState, + val privateOutboxRelayList: PrivateStorageRelayListState, + val localRelayList: LocalRelayListState, + val scope: CoroutineScope, +) { + fun mergeLists( + kind3: Set, + outbox: Set, + inbox: Set, + private: Set, + local: Set, + ): Set { + return kind3 + outbox + inbox + private + local + } + + val flow: StateFlow> = + combine( + followsOutboxRelayList.flow, + nip65RelayList.outboxFlow, + nip65RelayList.inboxFlow, + privateOutboxRelayList.flow, + localRelayList.flow, + ::mergeLists, + ) + .onStart { + emit( + mergeLists( + followsOutboxRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + ), + ) + } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + mergeLists( + followsOutboxRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt new file mode 100644 index 0000000000..995ca9a61d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.serverList + +import android.R.attr.host +import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS +import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import org.czeal.rfc3986.URIReference + +class MergedServerListState( + val fileServers: StateFlow>, + val blossomServers: StateFlow>, + val scope: CoroutineScope, +) { + fun host(url: String): String = + try { + URIReference.parse(url).host.value + } catch (e: Exception) { + url + } + + fun mergeServerList( + nip96: List?, + blossom: List?, + ): List { + val nip96servers = nip96?.map { ServerName(host(it), it, ServerType.NIP96) } ?: emptyList() + val blossomServers = blossom?.map { ServerName(host(it), it, ServerType.Blossom) } ?: emptyList() + + val result = (nip96servers + blossomServers).ifEmpty { DEFAULT_MEDIA_SERVERS } + + return result + ServerName("NIP95", "", ServerType.NIP95) + } + + val liveServerList: StateFlow> by lazy { + combine(fileServers, blossomServers) { nip96s, blossoms -> + mergeServerList(nip96s, blossoms) + } + .onStart { emit(mergeServerList(fileServers.value, blossomServers.value)) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt new file mode 100644 index 0000000000..95afd53955 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.serverList + +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState +import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState +import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class TrustedRelayListsState( + val nip65RelayList: Nip65RelayListState, + val privateOutboxRelayList: PrivateStorageRelayListState, + val localRelayList: LocalRelayListState, + val dmRelayList: DmRelayListState, + val searchRelayListState: SearchRelayListState, + val scope: CoroutineScope, +) { + fun mergeLists( + nip65: Set, + private: Set, + local: Set, + dm: Set, + search: Set, + ): Set { + return nip65 + private + local + dm + search + } + + val flow: StateFlow> = + combine( + nip65RelayList.allFlow, + privateOutboxRelayList.flow, + localRelayList.flow, + dmRelayList.flow, + searchRelayListState.flow, + ::mergeLists, + ) + .onStart { + emit( + mergeLists( + nip65RelayList.allFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + dmRelayList.flow.value, + searchRelayListState.flow.value, + ), + ) + } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + mergeLists( + nip65RelayList.allFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + dmRelayList.flow.value, + searchRelayListState.flow.value, + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt new file mode 100644 index 0000000000..1d11b9680a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds + +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.mapOfSet +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlin.collections.forEach +import kotlin.collections.ifEmpty + +class CommunityRelayLoader { + companion object { + fun communitiesPerRelay( + communityNotes: Array, + cache: LocalCache, + ): Map> { + return mapOfSet { + communityNotes.forEach { communityNote -> + val relays = + (communityNote.note.event as? CommunityDefinitionEvent)?.relayUrls() + ?.ifEmpty { null } + ?: cache.relayHints.hintsForAddress(communityNote.note.idHex) + + relays.forEach { + add(it, communityNote.note.idHex) + } + } + } + } + + fun communitiesPerRelaySnapshot( + communities: Set, + cache: LocalCache, + transformation: (Map>) -> T, + ): T { + val noteMetadata = + communities.mapNotNull { addressId -> + cache.checkGetOrCreateAddressableNote(addressId)?.flow()?.metadata?.stateFlow?.value + }.toTypedArray() + return transformation(communitiesPerRelay(noteMetadata, cache)) + } + + fun toCommunitiesPerRelayFlow( + communities: Set, + cache: LocalCache, + transformation: (Map>) -> T, + ): Flow { + val noteMetadataFlows = + communities.mapNotNull { addressId -> + cache.checkGetOrCreateAddressableNote(addressId)?.flow()?.metadata?.stateFlow + } + + return combine(noteMetadataFlows) { communityNotes -> + transformation(communitiesPerRelay(communityNotes, cache)) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt new file mode 100644 index 0000000000..4f7fa47508 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds + +import com.vitorpamplona.amethyst.model.ALL_FOLLOWS +import com.vitorpamplona.amethyst.model.AROUND_ME +import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsFeedFlow +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.AroundMeFeedFlow +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalFeedFlow +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.NoteFeedFlow +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.unknown.UnknownFeedFlow +import com.vitorpamplona.amethyst.service.location.LocationState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest + +class FeedTopNavFilterState( + val feedFilterListName: MutableStateFlow, + val allFollows: StateFlow, + val locationFlow: StateFlow, + val followsRelays: StateFlow>, + val signer: NostrSigner, + val scope: CoroutineScope, +) { + fun loadFlowsFor(listName: String): IFeedFlowsType = + when (listName) { + GLOBAL_FOLLOWS -> GlobalFeedFlow(followsRelays) + ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays) + AROUND_ME -> AroundMeFeedFlow(locationFlow, followsRelays) + else -> { + val note = LocalCache.checkGetOrCreateAddressableNote(listName) + if (note != null) { + NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays) + } else { + UnknownFeedFlow(listName) + } + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + val flow: StateFlow = + feedFilterListName.transformLatest { listName -> + emitAll(loadFlowsFor(listName).flow()) + } + .onStart { + loadFlowsFor(feedFilterListName.value).startValue(this) + } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + AuthorsByOutboxTopNavFilter(emptySet()), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayServiceStatus.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt similarity index 78% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayServiceStatus.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt index 99c3191863..5dc1a6b457 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayServiceStatus.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt @@ -18,16 +18,13 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.service.relays +package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.ammolite.relays.NostrClient +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector -sealed class RelayServiceStatus { - data class Active( - val client: NostrClient, - ) : RelayServiceStatus() +interface IFeedFlowsType { + fun flow(): Flow - object Off : RelayServiceStatus() - - object Connecting : RelayServiceStatus() + suspend fun startValue(collector: FlowCollector) } diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/IPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt similarity index 72% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/IPerRelayFilter.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt index 0071534387..2ec3d0a43d 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/IPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavFilter.kt @@ -18,23 +18,19 @@ * 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.ammolite.relays.filters +package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlinx.coroutines.flow.Flow -interface IPerRelayFilter { - fun toRelay(forRelay: String): Filter +interface IFeedTopNavFilter { + fun matchAuthor(pubkey: HexKey): Boolean - fun toJson(forRelay: String): String + fun match(noteEvent: Event): Boolean - fun match( - event: Event, - forRelay: String, - ): Boolean + fun toPerRelayFlow(cache: LocalCache): Flow - fun toDebugJson(): String - - // This only exists because some relays confuse empty lists with null lists - fun isValidFor(url: String): Boolean + fun startValue(cache: LocalCache): IFeedTopNavPerRelayFilterSet } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt new file mode 100644 index 0000000000..4cbff4957a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilter.kt @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds + +interface IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt new file mode 100644 index 0000000000..5f922fbeb5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedTopNavPerRelayFilterSet.kt @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds + +interface IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt new file mode 100644 index 0000000000..8d125d98af --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds + +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.mapOfSet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class MergedTopFeedAuthorListsState( + val homeNavFilter: StateFlow, + val videoNavFilter: StateFlow, + val discoveryNavFilter: StateFlow, + val notificationNavFilter: StateFlow, + val scope: CoroutineScope, +) { + fun authorList(navFilter: IFeedTopNavPerRelayFilterSet): Map?> { + return when (navFilter) { + is AllCommunitiesTopNavPerRelayFilterSet -> emptyMap() + is AllFollowsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } + is AuthorsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } + is GlobalTopNavPerRelayFilterSet -> emptyMap() + is HashtagTopNavPerRelayFilterSet -> emptyMap() + is LocationTopNavPerRelayFilterSet -> emptyMap() + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } + is SingleCommunityTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } + else -> emptyMap() + } + } + + fun mergeLists( + homeNavFilter: IFeedTopNavPerRelayFilterSet, + videoNavFilter: IFeedTopNavPerRelayFilterSet, + discoveryNavFilter: IFeedTopNavPerRelayFilterSet, + notificationNavFilter: IFeedTopNavPerRelayFilterSet, + ): Map> { + return mapOfSet { + authorList(homeNavFilter).forEach { (relay, authors) -> + authors?.let { add(relay, authors) } + } + + authorList(videoNavFilter).forEach { (relay, authors) -> + authors?.let { add(relay, authors) } + } + + authorList(discoveryNavFilter).forEach { (relay, authors) -> + authors?.let { add(relay, authors) } + } + + authorList(notificationNavFilter).forEach { (relay, authors) -> + authors?.let { add(relay, authors) } + } + } + } + + val flow: StateFlow>> = + combine( + homeNavFilter, + videoNavFilter, + discoveryNavFilter, + notificationNavFilter, + ::mergeLists, + ).onStart { + emit( + mergeLists( + homeNavFilter.value, + videoNavFilter.value, + discoveryNavFilter.value, + notificationNavFilter.value, + ), + ) + } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyMap(), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt new file mode 100644 index 0000000000..9a68e1e63c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds + +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.unknown.UnknownTopNavPerRelayFilterSet +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest + +class OutboxLoaderState( + topNavFilter: StateFlow, + cache: LocalCache, + scope: CoroutineScope, +) { + @OptIn(ExperimentalCoroutinesApi::class) + val flow: StateFlow = + topNavFilter.transformLatest { filterSettings -> + emitAll(filterSettings.toPerRelayFlow(cache)) + }.onStart { + emit(topNavFilter.value.startValue(cache)) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Companion.Eagerly, + UnknownTopNavPerRelayFilterSet, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt new file mode 100644 index 0000000000..11e520899c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds + +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.utils.mapOfSet +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlin.collections.ifEmpty + +class OutboxRelayLoader { + companion object { + private fun authorsPerRelay( + outboxRelayNotes: Array, + cache: LocalCache, + ): Map> { + return mapOfSet { + outboxRelayNotes.forEach { outboxNote -> + val relays = + (outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.ifEmpty { null } + ?: outboxNote.note.author?.pubkeyHex ?.let { cache.relayHints.hintsForKey(it) } + + relays?.forEach { + add(it, outboxNote.note.idHex) + } + } + } + } + + fun authorsPerRelaySnapshot( + authors: Set, + cache: LocalCache, + transformation: (Map>) -> T, + ): T { + val noteMetadata = + authors.map { pubkeyHex -> + cache.getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex)).flow().metadata.stateFlow.value + }.toTypedArray() + return transformation(authorsPerRelay(noteMetadata, cache)) + } + + fun toAuthorsPerRelayFlow( + authors: Set, + cache: LocalCache, + transformation: (Map>) -> T, + ): Flow { + val noteMetadataFlows = + authors.map { pubkeyHex -> + val note = cache.getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex)) + note.flow().metadata.stateFlow + } + + return combine(noteMetadataFlows) { outboxRelays -> + transformation(authorsPerRelay(outboxRelays, cache)) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt new file mode 100644 index 0000000000..2d6515f079 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.CommunityRelayLoader +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNotes +import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHashes +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHashes +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId +import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine + +/** + * This is a big OR filter on all fields. + */ +@Immutable +class AllFollowsByOutboxTopNavFilter( + val authors: Set? = null, + val hashtags: Set? = null, + val geotags: Set? = null, + val communities: Set? = null, + val defaultRelays: StateFlow>, +) : IFeedTopNavFilter { + val geotagScopes: Set? = geotags?.mapTo(mutableSetOf()) { GeohashId.Companion.toScope(it) } + val hashtagScopes: Set? = hashtags?.mapTo(mutableSetOf()) { HashtagId.Companion.toScope(it) } + + override fun matchAuthor(pubkey: HexKey): Boolean { + return authors == null || pubkey in authors + } + + override fun match(noteEvent: Event): Boolean { + return if (noteEvent is LiveActivitiesEvent) { + (authors != null && noteEvent.participantsIntersect(authors)) || + (hashtags != null && noteEvent.isTaggedHashes(hashtags)) || + (geotags != null && noteEvent.isTaggedGeoHashes(geotags)) || + (communities != null && noteEvent.isTaggedAddressableNotes(communities)) + } else if (noteEvent is CommentEvent) { + // ignore follows and checks only the root scope + (authors != null && noteEvent.pubKey in authors) || + (hashtags != null && noteEvent.isTaggedHashes(hashtags)) || + (hashtagScopes != null && noteEvent.isTaggedScopes(hashtagScopes)) || + (geotags != null && noteEvent.isTaggedGeoHashes(geotags)) || + (geotagScopes != null && noteEvent.isTaggedScopes(geotagScopes)) || + (communities != null && noteEvent.isTaggedAddressableNotes(communities)) + } else { + (authors != null && noteEvent.pubKey in authors) || + (hashtags != null && noteEvent.isTaggedHashes(hashtags)) || + (geotags != null && noteEvent.isTaggedGeoHashes(geotags)) || + (communities != null && noteEvent.isTaggedAddressableNotes(communities)) + } + } + + override fun toPerRelayFlow(cache: LocalCache): Flow { + val authorsPerRelay = + if (authors != null) { + OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } + } else { + MutableStateFlow(emptyMap()) + } + val communitiesPerRelay = + if (communities != null) { + CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) { it } + } else { + MutableStateFlow(emptyMap()) + } + + return combine(authorsPerRelay, communitiesPerRelay, defaultRelays) { perRelayAuthors, perRelayCommunities, default -> + val allRelays = (perRelayAuthors.keys + perRelayCommunities.keys).ifEmpty { default } + + AllFollowsByOutboxTopNavPerRelayFilterSet( + allRelays.associateWith { + AllFollowsByOutboxTopNavPerRelayFilter( + authors = perRelayAuthors[it], + hashtags = hashtags, + geotags = geotags, + communities = perRelayCommunities[it], + ) + }, + ) + } + } + + override fun startValue(cache: LocalCache): AllFollowsByOutboxTopNavPerRelayFilterSet { + val authorsPerRelay = + if (authors != null) { + OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } + } else { + emptyMap() + } + val communitiesPerRelay = + if (communities != null) { + CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) { it } + } else { + emptyMap() + } + + val allRelays = (authorsPerRelay.keys + communitiesPerRelay.keys).ifEmpty { defaultRelays.value } + + return AllFollowsByOutboxTopNavPerRelayFilterSet( + allRelays.associateWith { + AllFollowsByOutboxTopNavPerRelayFilter( + authors = authorsPerRelay[it], + hashtags = hashtags, + geotags = geotags, + communities = communitiesPerRelay[it], + ) + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3BasicRelaySetupInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilter.kt similarity index 60% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3BasicRelaySetupInfo.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilter.kt index 49db64fce5..49f1aa7e6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3BasicRelaySetupInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilter.kt @@ -18,21 +18,23 @@ * 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.relays.kind3 +package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows import androidx.compose.runtime.Immutable -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.quartz.nip01Core.relay.RelayStat +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter +import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId +import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId +/** + * This is a big OR filter. + */ @Immutable -data class Kind3BasicRelaySetupInfo( - val url: String, - val read: Boolean, - val write: Boolean, - val feedTypes: Set, - val relayStat: RelayStat, - val paidRelay: Boolean = false, -) { - val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url) +class AllFollowsByOutboxTopNavPerRelayFilter( + val authors: Set? = null, + val hashtags: Set? = null, + val geotags: Set? = null, + val communities: Set? = null, +) : IFeedTopNavPerRelayFilter { + val geotagScopes: Set? = geotags?.mapTo(mutableSetOf()) { GeohashId.toScope(it) } + val hashtagScopes: Set? = hashtags?.mapTo(mutableSetOf()) { HashtagId.toScope(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilterSet.kt new file mode 100644 index 0000000000..7352e976b6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilterSet.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class AllFollowsByOutboxTopNavPerRelayFilterSet( + val set: Map, +) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt new file mode 100644 index 0000000000..ed162f657a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows + +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map + +class AllFollowsFeedFlow( + val allFollows: StateFlow, + val followsRelays: StateFlow>, +) : IFeedFlowsType { + fun convert(kind3: FollowListState.Kind3Follows?): AllFollowsByOutboxTopNavFilter { + return if (kind3 != null) { + AllFollowsByOutboxTopNavFilter( + authors = kind3.authors, + hashtags = kind3.hashtags, + geotags = kind3.geotags, + communities = kind3.communities, + defaultRelays = followsRelays, + ) + } else { + AllFollowsByOutboxTopNavFilter( + authors = emptySet(), + defaultRelays = followsRelays, + ) + } + } + + override fun flow() = allFollows.map(::convert) + + override suspend fun startValue(collector: FlowCollector) { + collector.emit(convert(allFollows.value)) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt new file mode 100644 index 0000000000..028931a18e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeExpander.kt @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe + +import com.fonfon.kgeohash.GeoHash + +fun compute50kmLine(geoHash: GeoHash): List { + val hashes = mutableListOf() + + hashes.add(geoHash.toString()) + + var currentGeoHash = geoHash + repeat(5) { + currentGeoHash = currentGeoHash.westernNeighbour + hashes.add(currentGeoHash.toString()) + } + + currentGeoHash = geoHash + repeat(5) { + currentGeoHash = currentGeoHash.easternNeighbour + hashes.add(currentGeoHash.toString()) + } + + return hashes +} + +fun compute50kmRange(geoHash: GeoHash): List { + val hashes = mutableListOf() + + hashes.addAll(compute50kmLine(geoHash)) + + var currentGeoHash = geoHash + repeat(5) { + currentGeoHash = currentGeoHash.northernNeighbour + hashes.addAll(compute50kmLine(currentGeoHash)) + } + + currentGeoHash = geoHash + repeat(5) { + currentGeoHash = currentGeoHash.southernNeighbour + hashes.addAll(compute50kmLine(currentGeoHash)) + } + + return hashes +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt new file mode 100644 index 0000000000..51207c0286 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.service.location.LocationState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map + +class AroundMeFeedFlow( + val location: StateFlow, + val allFollowRelays: StateFlow>, +) : IFeedFlowsType { + fun convert(result: LocationState.LocationResult): LocationTopNavFilter { + return if (result is LocationState.LocationResult.Success) { + // 2 neighbors deep = 25x25km + LocationTopNavFilter( + geotags = compute50kmRange(result.geoHash).toSet(), + relays = allFollowRelays, + ) + } else { + // empty feed until we have a successful geohash + LocationTopNavFilter( + geotags = emptySet(), + relays = allFollowRelays, + ) + } + } + + override fun flow() = location.map(::convert) + + override suspend fun startValue(collector: FlowCollector) { + collector.emit(convert(location.value)) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt new file mode 100644 index 0000000000..3016de1622 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHashes +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map + +@Immutable +class LocationTopNavFilter( + val geotags: Set, + val relays: StateFlow>, +) : IFeedTopNavFilter { + val geotagScopes: Set = geotags.mapTo(mutableSetOf()) { GeohashId.Companion.toScope(it) } + + override fun matchAuthor(pubkey: HexKey): Boolean = true + + override fun match(noteEvent: Event): Boolean { + if (geotags.isEmpty()) return false + + return if (noteEvent is CommentEvent) { + noteEvent.isTaggedGeoHashes(geotags) || + noteEvent.isTaggedScopes(geotagScopes) + } else { + noteEvent.isTaggedGeoHashes(geotags) + } + } + + override fun toPerRelayFlow(cache: LocalCache): Flow { + return relays.map { + LocationTopNavPerRelayFilterSet(it.associateWith { LocationTopNavPerRelayFilter(geotags) }) + } + } + + override fun startValue(cache: LocalCache): LocationTopNavPerRelayFilterSet { + return LocationTopNavPerRelayFilterSet(relays.value.associateWith { LocationTopNavPerRelayFilter(geotags) }) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt new file mode 100644 index 0000000000..219c107192 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilter.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter +import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId + +@Immutable +class LocationTopNavPerRelayFilter( + val geotags: Set, +) : IFeedTopNavPerRelayFilter { + val geotagScopes: Set = geotags.mapTo(mutableSetOf()) { GeohashId.toScope(it) } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt new file mode 100644 index 0000000000..85564aac7c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavPerRelayFilterSet.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class LocationTopNavPerRelayFilterSet( + val set: Map, +) : IFeedTopNavPerRelayFilterSet diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayBriefInfoCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt similarity index 61% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayBriefInfoCache.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt index e57f457805..0a2141df27 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayBriefInfoCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt @@ -18,29 +18,23 @@ * 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.ammolite.relays +package com.vitorpamplona.amethyst.model.topNavFeeds.global -import android.util.LruCache -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow -object RelayBriefInfoCache { - val cache = LruCache(50) +class GlobalFeedFlow( + val relays: StateFlow>, +) : IFeedFlowsType { + val default = GlobalTopNavFilter(relays) - @Immutable - class RelayBriefInfo( - val url: String, - ) { - val displayUrl: String = RelayUrlFormatter.displayUrl(url).intern() - val favIcon: String = "https://$displayUrl/favicon.ico".intern() - } + override fun flow() = MutableStateFlow(default) - fun get(url: String): RelayBriefInfo { - val info = cache[url] - if (info != null) return info - - val newInfo = RelayBriefInfo(url) - cache.put(url, newInfo) - return newInfo + override suspend fun startValue(collector: FlowCollector) { + collector.emit(default) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt new file mode 100644 index 0000000000..a8ab72b43c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.global + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map + +@Immutable +class GlobalTopNavFilter( + val relays: StateFlow>, +) : IFeedTopNavFilter { + override fun matchAuthor(pubkey: HexKey): Boolean = true + + override fun match(noteEvent: Event) = true + + override fun toPerRelayFlow(cache: LocalCache): Flow { + return relays.map { + GlobalTopNavPerRelayFilterSet(it.associateWith { GlobalTopNavPerRelayFilter }) + } + } + + override fun startValue(cache: LocalCache): GlobalTopNavPerRelayFilterSet { + return GlobalTopNavPerRelayFilterSet( + relays.value.associateWith { GlobalTopNavPerRelayFilter }, + ) + } +} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/TypedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt similarity index 77% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/TypedFilter.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt index 6e6201042a..772a298dca 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/TypedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilter.kt @@ -18,14 +18,10 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.ammolite.relays +package com.vitorpamplona.amethyst.model.topNavFeeds.global -import com.vitorpamplona.ammolite.relays.filters.IPerRelayFilter +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter -class TypedFilter( - val types: Set, - val filter: IPerRelayFilter, -) { - // This only exists because some relays confuse empty lists with null lists - fun isValidFor(url: String) = filter.isValidFor(url) -} +@Immutable +object GlobalTopNavPerRelayFilter : IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt new file mode 100644 index 0000000000..2e30e5dc53 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavPerRelayFilterSet.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.global + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class GlobalTopNavPerRelayFilterSet( + val set: Map, +) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt new file mode 100644 index 0000000000..c261a34581 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHashes +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map + +@Immutable +class HashtagTopNavFilter( + val hashtags: Set, + val relays: StateFlow>, +) : IFeedTopNavFilter { + val hashtagScopes: Set = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) } + + override fun matchAuthor(pubkey: HexKey): Boolean = true + + override fun match(noteEvent: Event): Boolean { + return if (noteEvent is CommentEvent) { + noteEvent.isTaggedHashes(hashtags) || noteEvent.isTaggedScopes(hashtagScopes) + } else { + noteEvent.isTaggedHashes(hashtags) + } + } + + override fun toPerRelayFlow(cache: LocalCache): Flow { + return relays.map { + HashtagTopNavPerRelayFilterSet( + it.associateWith { HashtagTopNavPerRelayFilter(hashtags) }, + ) + } + } + + override fun startValue(cache: LocalCache): HashtagTopNavPerRelayFilterSet { + return HashtagTopNavPerRelayFilterSet( + relays.value.associateWith { + HashtagTopNavPerRelayFilter(hashtags) + }, + ) + } +} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelaySetupInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt similarity index 74% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelaySetupInfo.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt index 7d5868018e..8c5a0406f2 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelaySetupInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilter.kt @@ -18,23 +18,15 @@ * 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.ammolite.relays +package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter +import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId @Immutable -data class RelaySetupInfo( - val url: String, - val read: Boolean, - val write: Boolean, - val feedTypes: Set, -) - -@Immutable -data class RelaySetupInfoToConnect( - val url: String, - val forceProxy: Boolean, - val read: Boolean, - val write: Boolean, - val feedTypes: Set, -) +class HashtagTopNavPerRelayFilter( + val hashtags: Set, +) : IFeedTopNavPerRelayFilter { + val hashtagScopes: Set = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt new file mode 100644 index 0000000000..35c5481b0c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavPerRelayFilterSet.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.hashtag + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class HashtagTopNavPerRelayFilterSet( + val set: Map, +) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt new file mode 100644 index 0000000000..b30f723581 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased + +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.transformLatest + +class NoteFeedFlow( + val metadataFlow: StateFlow, + val signer: NostrSigner, + val allFollowRelays: StateFlow>, +) : IFeedFlowsType { + suspend fun FlowCollector.process(noteEvent: Event) { + when (noteEvent) { + is PeopleListEvent -> { + if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { + emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicUsersAndWords().users)) + + noteEvent.publicAndPrivateUsersAndWords(signer)?.let { + emit(MutedAuthorsByOutboxTopNavFilter(it.users)) + } + } else { + emit(AuthorsByOutboxTopNavFilter(noteEvent.publicUsersAndWords().users)) + + noteEvent.publicAndPrivateUsersAndWords(signer)?.let { + emit(AuthorsByOutboxTopNavFilter(it.users)) + } + } + } + is MuteListEvent -> { + emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicUsersAndWords().users)) + + noteEvent.publicAndPrivateUsersAndWords(signer)?.let { + emit(MutedAuthorsByOutboxTopNavFilter(it.users)) + } + } + is FollowListEvent -> { + emit(AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet())) + } + is CommunityListEvent -> { + emit(AllCommunitiesTopNavFilter(noteEvent.publicCommunityIds().toSet())) + + noteEvent.publicAndPrivateCommunities(signer)?.let { + val communities = it.map { it.addressId }.toSet() + emit(AllCommunitiesTopNavFilter(communities)) + } + } + is HashtagListEvent -> { + emit(HashtagTopNavFilter(noteEvent.publicHashtags().toSet(), allFollowRelays)) + + noteEvent.publicAndPrivateHashtag(signer)?.let { + emit(HashtagTopNavFilter(it, allFollowRelays)) + } + } + is GeohashListEvent -> { + emit(LocationTopNavFilter(noteEvent.publicGeohashes().toSet(), allFollowRelays)) + + noteEvent.publicAndPrivateGeohash(signer)?.let { + emit(LocationTopNavFilter(it, allFollowRelays)) + } + } + is CommunityDefinitionEvent -> { + SingleCommunityTopNavFilter( + community = noteEvent.addressTag(), + authors = noteEvent.moderatorKeys().toSet().ifEmpty { null }, + relays = noteEvent.relayUrls().toSet(), + ) + } + else -> AuthorsByOutboxTopNavFilter(emptySet()) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun flow() = + metadataFlow.transformLatest { noteState -> + val noteEvent = noteState?.note?.event + if (noteEvent == null) { + AuthorsByOutboxTopNavFilter(emptySet()) + } else { + process(noteEvent) + } + } + + override suspend fun startValue(collector: FlowCollector) { + val noteEvent = metadataFlow.value?.note?.event + if (noteEvent == null) { + AuthorsByOutboxTopNavFilter(emptySet()) + } else { + collector.process(noteEvent) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt new file mode 100644 index 0000000000..0dca95f15a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.CommunityRelayLoader +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNotes +import kotlinx.coroutines.flow.Flow + +@Immutable +class AllCommunitiesTopNavFilter( + val communities: Set, +) : IFeedTopNavFilter { + override fun matchAuthor(pubkey: HexKey): Boolean = true + + override fun match(noteEvent: Event): Boolean { + return noteEvent.isTaggedAddressableNotes(communities) + } + + fun convert(map: Map>) = + AllCommunitiesTopNavPerRelayFilterSet( + map.mapValues { AllCommunitiesTopNavPerRelayFilter(it.value) }, + ) + + override fun toPerRelayFlow(cache: LocalCache): Flow { + return CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) { + convert(it) + } + } + + override fun startValue(cache: LocalCache): AllCommunitiesTopNavPerRelayFilterSet { + return CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) { + convert(it) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt new file mode 100644 index 0000000000..dd532c3e99 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilter.kt @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter + +class AllCommunitiesTopNavPerRelayFilter( + val communities: Set, +) : IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt new file mode 100644 index 0000000000..061c40e5a9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavPerRelayFilterSet.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class AllCommunitiesTopNavPerRelayFilterSet( + val set: Map, +) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt new file mode 100644 index 0000000000..14697499b0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import kotlinx.coroutines.flow.Flow + +@Immutable +class AuthorsByOutboxTopNavFilter( + val authors: Set, +) : IFeedTopNavFilter { + override fun matchAuthor(pubkey: HexKey) = pubkey in authors + + override fun match(noteEvent: Event): Boolean { + return if (noteEvent is LiveActivitiesEvent) { + noteEvent.participantsIntersect(authors) + } else { + noteEvent.pubKey in authors + } + } + + fun convert(map: Map>) = + AuthorsByOutboxTopNavPerRelayFilterSet( + map.mapValues { AuthorsByOutboxTopNavPerRelayFilter(it.value) }, + ) + + override fun toPerRelayFlow(cache: LocalCache): Flow { + return OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert) + } + + override fun startValue(cache: LocalCache): AuthorsByOutboxTopNavPerRelayFilterSet { + return OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilter.kt new file mode 100644 index 0000000000..1c1d215ea3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilter.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter + +@Immutable +class AuthorsByOutboxTopNavPerRelayFilter( + val authors: Set, +) : IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilterSet.kt new file mode 100644 index 0000000000..4564dc5fbd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilterSet.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class AuthorsByOutboxTopNavPerRelayFilterSet( + val set: Map, +) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt new file mode 100644 index 0000000000..3d70161929 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +@Immutable +class SingleCommunityTopNavFilter( + val community: String, + val authors: Set?, + val relays: Set, +) : IFeedTopNavFilter { + override fun matchAuthor(pubkey: HexKey) = authors == null || pubkey in authors + + override fun match(noteEvent: Event): Boolean { + return if (noteEvent is LiveActivitiesEvent) { + (authors != null && noteEvent.participantsIntersect(authors)) || noteEvent.isTaggedAddressableNote(community) + } else if (noteEvent is CommentEvent) { + (authors != null && noteEvent.pubKey in authors) || noteEvent.isTaggedAddressableNote(community) + } else { + (authors != null && noteEvent.pubKey in authors) || noteEvent.isTaggedAddressableNote(community) + } + } + + override fun toPerRelayFlow(cache: LocalCache): Flow { + // relay field takes priority + if (relays.isNotEmpty()) { + return MutableStateFlow( + SingleCommunityTopNavPerRelayFilterSet( + relays.associateWith { + SingleCommunityTopNavPerRelayFilter(community, authors) + }, + ), + ) + } + + if (authors != null) { + // go by authors + return OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { + SingleCommunityTopNavPerRelayFilterSet( + it.mapValues { + SingleCommunityTopNavPerRelayFilter(community, it.value) + }, + ) + } + } + + // go by hints + return MutableStateFlow( + SingleCommunityTopNavPerRelayFilterSet( + cache.relayHints.hintsForAddress(community).associateWith { + SingleCommunityTopNavPerRelayFilter(community, authors) + }, + ), + ) + } + + override fun startValue(cache: LocalCache): SingleCommunityTopNavPerRelayFilterSet { + // relay field takes priority + if (relays.isNotEmpty()) { + return SingleCommunityTopNavPerRelayFilterSet( + relays.associateWith { + SingleCommunityTopNavPerRelayFilter(community, authors) + }, + ) + } + + if (authors != null) { + // go by authors + return OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { + SingleCommunityTopNavPerRelayFilterSet( + it.mapValues { + SingleCommunityTopNavPerRelayFilter(community, it.value) + }, + ) + } + } + + // go by hints + return SingleCommunityTopNavPerRelayFilterSet( + cache.relayHints.hintsForAddress(community).associateWith { + SingleCommunityTopNavPerRelayFilter(community, authors) + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt new file mode 100644 index 0000000000..596af8a4c5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilter.kt @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter + +@Immutable +class SingleCommunityTopNavPerRelayFilter( + val community: String, + val authors: Set?, +) : IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt new file mode 100644 index 0000000000..c376579fa0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavPerRelayFilterSet.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class SingleCommunityTopNavPerRelayFilterSet( + val set: Map, +) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt new file mode 100644 index 0000000000..d98676d6d9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxRelayLoader +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import kotlinx.coroutines.flow.Flow + +@Immutable +class MutedAuthorsByOutboxTopNavFilter( + val authors: Set, +) : IFeedTopNavFilter { + override fun matchAuthor(pubkey: HexKey) = pubkey in authors + + override fun match(noteEvent: Event): Boolean { + return if (noteEvent is LiveActivitiesEvent) { + noteEvent.participantsIntersect(authors) + } else { + noteEvent.pubKey in authors + } + } + + fun convert(map: Map>) = + MutedAuthorsByOutboxTopNavPerRelayFilterSet( + map.mapValues { MutedAuthorsByOutboxTopNavPerRelayFilter(it.value) }, + ) + + override fun toPerRelayFlow(cache: LocalCache): Flow { + return OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert) + } + + override fun startValue(cache: LocalCache): MutedAuthorsByOutboxTopNavPerRelayFilterSet { + return OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilter.kt new file mode 100644 index 0000000000..1ee39cc473 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilter.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter + +@Immutable +class MutedAuthorsByOutboxTopNavPerRelayFilter( + val authors: Set, +) : IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilterSet.kt new file mode 100644 index 0000000000..e2760be428 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilterSet.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class MutedAuthorsByOutboxTopNavPerRelayFilterSet( + val set: Map, +) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt new file mode 100644 index 0000000000..4596d30986 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.unknown + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableStateFlow + +class UnknownFeedFlow( + val feedName: String, +) : IFeedFlowsType { + override fun flow() = MutableStateFlow(UnknownTopNavFilter(feedName)) + + // empty feed + override suspend fun startValue(collector: FlowCollector) { + collector.emit(UnknownTopNavFilter(feedName)) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelayProposalSetupInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt similarity index 62% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelayProposalSetupInfo.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt index e1bbd9e1d0..0699da58bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelayProposalSetupInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt @@ -18,23 +18,27 @@ * 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.relays.recommendations +package com.vitorpamplona.amethyst.model.topNavFeeds.unknown import androidx.compose.runtime.Immutable -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.RelayStat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow @Immutable -data class Kind3RelayProposalSetupInfo( - val url: String, - val read: Boolean, - val write: Boolean, - val feedTypes: Set, - val relayStat: RelayStat, - val paidRelay: Boolean = false, - val users: List, -) { - val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url) +class UnknownTopNavFilter( + val feedName: String, +) : IFeedTopNavFilter { + override fun matchAuthor(pubkey: HexKey) = false + + override fun match(noteEvent: Event) = false + + override fun toPerRelayFlow(cache: LocalCache): Flow { + return MutableStateFlow(UnknownTopNavPerRelayFilterSet) + } + + override fun startValue(cache: LocalCache) = UnknownTopNavPerRelayFilterSet } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt similarity index 83% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt index b94ee3de89..556f596fc9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavPerRelayFilterSet.kt @@ -18,15 +18,8 @@ * 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.nip01Core.relay +package com.vitorpamplona.amethyst.model.topNavFeeds.unknown -enum class RelayState { - // Websocket connected - CONNECTED, +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet - // Websocket disconnecting - DISCONNECTING, - - // Websocket disconnected - DISCONNECTED, -} +object UnknownTopNavPerRelayFilterSet : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt new file mode 100644 index 0000000000..db2277eb80 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.torState + +import com.vitorpamplona.amethyst.ui.tor.TorType +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isOnion + +class TorRelayEvaluation( + val torSettings: TorRelaySettings, + val trustedRelayList: Set, + val dmRelayList: Set, +) { + fun useTor(relay: NormalizedRelayUrl): Boolean { + return if (torSettings.torType == TorType.OFF) { + false + } else { + if (relay.isLocalHost()) { + false + } else if (relay.isOnion()) { + torSettings.onionRelaysViaTor + } else if (relay in dmRelayList) { + torSettings.dmRelaysViaTor + } else if (relay in trustedRelayList) { + torSettings.trustedRelaysViaTor + } else { + torSettings.newRelaysViaTor + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt new file mode 100644 index 0000000000..77a59f42ce --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.torState + +import com.vitorpamplona.amethyst.ui.tor.TorType + +class TorRelaySettings( + val torType: TorType = TorType.OFF, + val onionRelaysViaTor: Boolean = true, + val dmRelaysViaTor: Boolean = false, + val trustedRelaysViaTor: Boolean = false, + val newRelaysViaTor: Boolean = false, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt new file mode 100644 index 0000000000..daa8384486 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.torState + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState +import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState +import com.vitorpamplona.amethyst.ui.tor.TorType +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class TorRelayState( + val trustedRelayState: TrustedRelayListsState, + val dmRelayState: DmRelayListState, + val settings: AccountSettings, + val scope: CoroutineScope, +) { + val torSettings = + combine( + settings.torSettings.torType, + settings.torSettings.onionRelaysViaTor, + settings.torSettings.dmRelaysViaTor, + settings.torSettings.trustedRelaysViaTor, + settings.torSettings.newRelaysViaTor, + ) { + torType: TorType, + onionRelaysViaTor: Boolean, + dmRelaysViaTor: Boolean, + trustedRelaysViaTor: Boolean, + newRelaysViaTor: Boolean, + -> + TorRelaySettings(torType, onionRelaysViaTor, dmRelaysViaTor, trustedRelaysViaTor, newRelaysViaTor) + }.onStart { + emit( + TorRelaySettings( + settings.torSettings.torType.value, + settings.torSettings.onionRelaysViaTor.value, + settings.torSettings.dmRelaysViaTor.value, + settings.torSettings.trustedRelaysViaTor.value, + settings.torSettings.newRelaysViaTor.value, + ), + ) + } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + TorRelaySettings( + settings.torSettings.torType.value, + settings.torSettings.onionRelaysViaTor.value, + settings.torSettings.dmRelaysViaTor.value, + settings.torSettings.trustedRelaysViaTor.value, + settings.torSettings.newRelaysViaTor.value, + ), + ) + + val flow = + combineTransform( + torSettings, + trustedRelayState.flow, + dmRelayState.flow, + ) { torSettings: TorRelaySettings, trustedRelayList: Set, dmRelayList: Set -> + emit(TorRelayEvaluation(torSettings, trustedRelayList, dmRelayList)) + }.onStart { + emit( + TorRelayEvaluation( + torSettings.value, + trustedRelayState.flow.value, + dmRelayState.flow.value, + ), + ) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + TorRelayEvaluation( + torSettings.value, + trustedRelayState.flow.value, + dmRelayState.flow.value, + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt index 9b1d652d0e..62c0d28bb8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt @@ -22,8 +22,9 @@ package com.vitorpamplona.amethyst.service import android.util.Log import android.util.LruCache +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CancellationException import okhttp3.Call @@ -49,25 +50,23 @@ object Nip11CachedRetriever { class RetrieveResultLoading : RetrieveResult(TimeUtils.now()) - private val relayInformationDocumentCache = LruCache(100) + private val relayInformationDocumentCache = LruCache(100) private val retriever = Nip11Retriever() - fun getFromCache(dirtyUrl: String): Nip11RelayInformation? { - val result = relayInformationDocumentCache.get(RelayUrlFormatter.getHttpsUrl(dirtyUrl)) ?: return null + fun getFromCache(relay: NormalizedRelayUrl): Nip11RelayInformation? { + val result = relayInformationDocumentCache.get(relay) ?: return null if (result is RetrieveResultSuccess) return result.data return null } suspend fun loadRelayInfo( - dirtyUrl: String, + relay: NormalizedRelayUrl, okHttpClient: (String) -> OkHttpClient, onInfo: (Nip11RelayInformation) -> Unit, - onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit, + onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit, ) { checkNotInMainThread() - val url = RelayUrlFormatter.getHttpsUrl(dirtyUrl) - val doc = relayInformationDocumentCache.get(url) - + val doc = relayInformationDocumentCache.get(relay) if (doc != null) { if (doc is RetrieveResultSuccess) { onInfo(doc.data) @@ -75,41 +74,37 @@ object Nip11CachedRetriever { if (TimeUtils.now() - doc.time < TimeUtils.ONE_MINUTE) { // just wait. } else { - retrieve(url, dirtyUrl, okHttpClient, onInfo, onError) + retrieve(relay, okHttpClient, onInfo, onError) } } else if (doc is RetrieveResultError) { if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) { - onError(dirtyUrl, doc.error, null) + onError(relay, doc.error, null) } else { - retrieve(url, dirtyUrl, okHttpClient, onInfo, onError) + retrieve(relay, okHttpClient, onInfo, onError) } } } else { - retrieve(url, dirtyUrl, okHttpClient, onInfo, onError) + retrieve(relay, okHttpClient, onInfo, onError) } } private suspend fun retrieve( - url: String, - dirtyUrl: String, + relay: NormalizedRelayUrl, okHttpClient: (String) -> OkHttpClient, onInfo: (Nip11RelayInformation) -> Unit, - onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit, + onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit, ) { - relayInformationDocumentCache.put(url, RetrieveResultLoading()) + relayInformationDocumentCache.put(relay, RetrieveResultLoading()) retriever.loadRelayInfo( - url = url, - dirtyUrl = dirtyUrl, + relay = relay, okHttpClient = okHttpClient, onInfo = { - checkNotInMainThread() - relayInformationDocumentCache.put(url, RetrieveResultSuccess(it)) + relayInformationDocumentCache.put(relay, RetrieveResultSuccess(it)) onInfo(it) }, - onError = { dirtyUrl, code, errorMsg -> - checkNotInMainThread() - relayInformationDocumentCache.put(url, RetrieveResultError(code, errorMsg)) - onError(url, code, errorMsg) + onError = { relay, code, errorMsg -> + relayInformationDocumentCache.put(relay, RetrieveResultError(code, errorMsg)) + onError(relay, code, errorMsg) }, ) } @@ -124,13 +119,13 @@ class Nip11Retriever { } suspend fun loadRelayInfo( - url: String, - dirtyUrl: String, + relay: NormalizedRelayUrl, okHttpClient: (String) -> OkHttpClient, onInfo: (Nip11RelayInformation) -> Unit, - onError: (String, ErrorCode, String?) -> Unit, + onError: (NormalizedRelayUrl, ErrorCode, String?) -> Unit, ) { checkNotInMainThread() + val url = relay.toHttp() try { val request: Request = Request @@ -154,16 +149,16 @@ class Nip11Retriever { if (it.isSuccessful) { onInfo(Nip11RelayInformation.fromJson(body)) } else { - onError(dirtyUrl, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString()) + onError(relay, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString()) } } catch (e: Exception) { if (e is CancellationException) throw e Log.e( "RelayInfoFail", - "Resulting Message from Relay $dirtyUrl in not parseable: $body", + "Resulting Message from Relay ${relay.url} in not parseable: $body", e, ) - onError(dirtyUrl, ErrorCode.FAIL_TO_PARSE_RESULT, e.message) + onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, e.message) } } } @@ -172,15 +167,15 @@ class Nip11Retriever { call: Call, e: IOException, ) { - Log.e("RelayInfoFail", "$dirtyUrl unavailable", e) - onError(dirtyUrl, ErrorCode.FAIL_TO_REACH_SERVER, e.message) + Log.e("RelayInfoFail", "${relay.url} unavailable", e) + onError(relay, ErrorCode.FAIL_TO_REACH_SERVER, e.message) } }, ) } catch (e: Exception) { if (e is CancellationException) throw e - Log.e("RelayInfoFail", "Invalid URL $dirtyUrl", e) - onError(dirtyUrl, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message) + Log.e("RelayInfoFail", "Invalid URL ${relay.url}", e) + onError(relay, ErrorCode.FAIL_TO_ASSEMBLE_URL, e.message) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index 19148b2ac2..844a5e6e1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -23,13 +23,14 @@ package com.vitorpamplona.amethyst.service import android.content.Context import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.collectSuccessfulOperations import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -39,6 +40,7 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.utils.collectSuccessfulOperations import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -173,6 +175,16 @@ class ZapPaymentHandler( val user: User? = null, ) + fun receivingRelaySet(userHex: HexKey): Set? { + return ( + LocalCache + .getAddressableNoteIfExists( + AdvertisedRelayListEvent.createAddressTag(userHex), + )?.event as? AdvertisedRelayListEvent + ) + ?.readRelaysNorm()?.toSet() + } + suspend fun signAllZapRequests( note: Note, pollOption: Int?, @@ -181,18 +193,6 @@ class ZapPaymentHandler( zapsToSend: List, onAllDone: suspend (List) -> Unit, ) { - val authorRelayList = - note.author - ?.pubkeyHex - ?.let { - ( - LocalCache - .getAddressableNoteIfExists( - AdvertisedRelayListEvent.createAddressTag(it), - )?.event as? AdvertisedRelayListEvent? - )?.readRelays() - }?.toSet() - collectSuccessfulOperations( items = zapsToSend, runRequestFor = { next: BaseZapSplitSetup, onReady -> @@ -203,18 +203,12 @@ class ZapPaymentHandler( } } } else if (next is ZapSplitSetup) { - val user = LocalCache.getUserIfExists(next.pubKeyHex) - val userRelayList = - ( - ( - LocalCache - .getAddressableNoteIfExists( - AdvertisedRelayListEvent.createAddressTag(next.pubKeyHex), - )?.event as? AdvertisedRelayListEvent? - )?.readRelays()?.toSet() ?: emptySet() - ) + (authorRelayList ?: emptySet()) + val authorRelayList = note.author?.let { receivingRelaySet(it.pubkeyHex) } ?: emptySet() + val userRelayList = receivingRelaySet(next.pubKeyHex) ?: emptySet() - prepareZapRequestIfNeeded(note, pollOption, message, zapType, user, userRelayList) { zapRequestJson -> + val user = LocalCache.getOrCreateUser(next.pubKeyHex) + + prepareZapRequestIfNeeded(note, pollOption, message, zapType, user, userRelayList + authorRelayList) { zapRequestJson -> onReady(ZapRequestReady(next, zapRequestJson, user)) } } @@ -387,7 +381,7 @@ class ZapPaymentHandler( message: String, zapType: LnZapEvent.ZapType, overrideUser: User? = null, - additionalRelays: Set? = null, + additionalRelays: Set? = null, onReady: (String?) -> Unit, ) { if (zapType != LnZapEvent.ZapType.NONZAP) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 5be226aeb0..b5ce69cc4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -25,11 +25,12 @@ import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences -import com.vitorpamplona.amethyst.launchAndWaitAll import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.tryAndWait +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal +import com.vitorpamplona.quartz.utils.launchAndWaitAll +import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType @@ -51,7 +52,7 @@ class RegisterAccounts( private suspend fun signAllAuths( notificationToken: String, - remainingTos: List>>, + remainingTos: List>>, output: MutableList, onReady: (List) -> Unit, ) { @@ -99,15 +100,15 @@ class RegisterAccounts( val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub) if (acc != null && acc.isWriteable()) { - val nip65Read = acc.backupNIP65RelayList?.readRelays() ?: emptyList() + val nip65Read = acc.backupNIP65RelayList?.readRelaysNorm() ?: emptyList() Log.d(tag, "Register Account ${it.npub} NIP65 Reads ${nip65Read.joinToString(", ")}") - val nip17Read = acc.backupDMRelayList?.relays() ?: emptyList() + val nip17Read = acc.backupDMRelayList?.relays() ?: emptyList() Log.d(tag, "Register Account ${it.npub} NIP17 Reads ${nip17Read.joinToString(", ")}") - val readKind3Relays = acc.backupContactList?.relays()?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList() + val readKind3Relays = acc.backupContactList?.relays()?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList() Log.d(tag, "Register Account ${it.npub} Kind3 Reads ${readKind3Relays.joinToString(", ")}") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt index 6268971c50..4ffa9f7b75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt @@ -39,7 +39,7 @@ class DualHttpClientManager( ) { val factory = OkHttpClientFactory(keyCache) - private val defaultHttpClient: StateFlow = + val defaultHttpClient: StateFlow = combine(proxyPortProvider, isMobileDataProvider) { proxy, mobile -> factory.buildHttpClient(proxy, mobile, userAgent) }.stateIn( @@ -48,7 +48,7 @@ class DualHttpClientManager( factory.buildHttpClient(proxyPortProvider.value, isMobileDataProvider.value, userAgent), ) - private val defaultHttpClientWithoutProxy: StateFlow = + val defaultHttpClientWithoutProxy: StateFlow = isMobileDataProvider .map { mobile -> factory.buildHttpClient(mobile, userAgent) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt index 51f8dcfdd2..00ec01942b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt @@ -20,27 +20,50 @@ */ package com.vitorpamplona.amethyst.service.okhttp +import android.system.Os.socket +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder -import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response class OkHttpWebSocket( - val url: String, - val forceProxy: Boolean, - val httpClient: (url: String, forceProxy: Boolean) -> OkHttpClient, + val url: NormalizedRelayUrl, + val httpClient: (url: NormalizedRelayUrl) -> OkHttpClient, val out: WebSocketListener, ) : WebSocket { private val listener = OkHttpWebsocketListener() + private var usingOkHttp: OkHttpClient? = null private var socket: okhttp3.WebSocket? = null - fun buildRequest() = Request.Builder().url(url.trim()).build() + fun buildRequest() = Request.Builder().url(url.url).build() + + override fun needsReconnect(): Boolean { + val myUsingOkHttp = usingOkHttp + if (myUsingOkHttp == null) return true + + val currentOkHttp = httpClient(url) + + val usingProxy = myUsingOkHttp.proxy + val currentProxy = currentOkHttp.proxy + + if (usingProxy != null && currentProxy != null && usingProxy != currentProxy) return true + if (usingProxy == null && currentProxy != null) return true + if (usingProxy != null && currentProxy == null) return true + + if (currentOkHttp.readTimeoutMillis != myUsingOkHttp.readTimeoutMillis) return true + if (currentOkHttp.writeTimeoutMillis != myUsingOkHttp.writeTimeoutMillis) return true + if (currentOkHttp.connectTimeoutMillis != myUsingOkHttp.connectTimeoutMillis) return true + if (currentOkHttp.callTimeoutMillis != myUsingOkHttp.callTimeoutMillis) return true + + return false + } override fun connect() { - socket = httpClient(url, forceProxy).newWebSocket(buildRequest(), listener) + usingOkHttp = httpClient(url) + socket = usingOkHttp?.newWebSocket(buildRequest(), listener) } inner class OkHttpWebsocketListener : okhttp3.WebSocketListener() { @@ -49,7 +72,7 @@ class OkHttpWebSocket( response: Response, ) = out.onOpen( response.receivedResponseAtMillis - response.sentRequestAtMillis, - response.headers.get("Sec-WebSocket-Extensions")?.contains("permessage-deflate") ?: false, + response.headers["Sec-WebSocket-Extensions"]?.contains("permessage-deflate") ?: false, ) override fun onMessage( @@ -73,31 +96,21 @@ class OkHttpWebSocket( webSocket: okhttp3.WebSocket, t: Throwable, response: Response?, - ) = out.onFailure(t, response?.message) + ) = out.onFailure(t, response?.code, response?.message) } class Builder( - val forceProxy: Boolean, - val httpClient: (String, Boolean) -> OkHttpClient, + val httpClient: (NormalizedRelayUrl) -> OkHttpClient, ) : WebsocketBuilder { // Called when connecting. override fun build( - url: String, + url: NormalizedRelayUrl, out: WebSocketListener, - ) = OkHttpWebSocket(url, forceProxy, httpClient, out) + ) = OkHttpWebSocket(url, httpClient, out) } - class BuilderFactory( - val httpClient: (String, Boolean) -> OkHttpClient, - ) : WebsocketBuilderFactory { - override fun build( - url: String, - forceProxy: Boolean, - ) = Builder(forceProxy, httpClient) - } - - override fun cancel() { - socket?.cancel() + override fun disconnect() { + socket?.close(1000, "Normal closure") } override fun send(msg: String): Boolean = socket?.send(msg) ?: false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt similarity index 57% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayService.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt index f03cafbb20..61133f089f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt @@ -18,31 +18,25 @@ * 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.service.relays +package com.vitorpamplona.amethyst.service.okhttp -import android.content.Context -import android.util.Log -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.launch +import com.vitorpamplona.amethyst.model.torState.TorRelayEvaluation +import com.vitorpamplona.amethyst.model.torState.TorRelaySettings +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow -class RelayService( - val context: Context, -) { - val status = - callbackFlow { - Log.d("RelayService", "Starting Relay Services") - trySend(RelayServiceStatus.Connecting) +class ProxySettingsAnchor() { + val flow: MutableStateFlow> = + MutableStateFlow( + MutableStateFlow( + TorRelayEvaluation( + torSettings = TorRelaySettings(), + trustedRelayList = emptySet(), + dmRelayList = emptySet(), + ), + ), + ) - // ServiceManager - - awaitClose { - Log.d("RelayService", "Stopping Relay Services") - launch { - // ServiceManager.pauseAndLogOff() - } - trySend(RelayServiceStatus.Off) - } - }.distinctUntilChanged() + var useProxy: (NormalizedRelayUrl) -> Boolean = { flow.value.value.useTor(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt index 168419aadb..42b1d85786 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.service.relayClient import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.ammolite.relays.datasources.EventCollector -import com.vitorpamplona.ammolite.relays.datasources.RelayInsertConfirmationCollector import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.EventCollector +import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.RelayInsertConfirmationCollector +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class CacheClientConnector( val client: NostrClient, @@ -38,8 +38,8 @@ class CacheClientConnector( val confirmationWatcher = RelayInsertConfirmationCollector(client) { eventId, relay -> - cache.markAsSeen(eventId, relay.brief) - markAsSeen(eventId, relay.brief) + cache.markAsSeen(eventId, relay.url) + markAsSeen(eventId, relay.url) } fun destroy() { @@ -49,6 +49,6 @@ class CacheClientConnector( private fun markAsSeen( eventId: HexKey, - relay: RelayBriefInfoCache.RelayBriefInfo, - ) = LocalCache.getNoteIfExists(eventId)?.addRelay(relay) + info: NormalizedRelayUrl, + ) = LocalCache.getNoteIfExists(eventId)?.addRelay(info) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayLogger.kt index 03416ce0d7..6b33107256 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayLogger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayLogger.kt @@ -21,9 +21,10 @@ package com.vitorpamplona.amethyst.service.relayClient import android.util.Log -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient /** * Listens to NostrClient's onNotify messages from the relay @@ -36,20 +37,20 @@ class RelayLogger( } private val clientListener = - object : NostrClient.Listener { + object : IRelayClientListener { /** A new message was received */ override fun onEvent( + relay: IRelayClient, + subId: String, event: Event, - subscriptionId: String, - relay: Relay, arrivalTime: Long, afterEOSE: Boolean, ) { - Log.d(TAG, "Relay onEVENT ${relay.url} ($subscriptionId - $afterEOSE) ${event.toJson()}") + Log.d(TAG, "Relay onEVENT ${relay.url} ($subId - $afterEOSE) ${event.toJson()}") } override fun onSend( - relay: Relay, + relay: IRelayClient, msg: String, success: Boolean, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt new file mode 100644 index 0000000000..4881062a95 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient + +import android.util.Log +import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager +import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager +import com.vitorpamplona.amethyst.service.okhttp.ProxySettingsAnchor +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class RelayProxyClientConnector( + val torProxySettingsAnchor: ProxySettingsAnchor, + val okHttpClients: DualHttpClientManager, + val connManager: ConnectivityManager, + val client: NostrClient, + val scope: CoroutineScope, +) { + @OptIn(FlowPreview::class) + val relayServices = + combine( + torProxySettingsAnchor.flow, + okHttpClients.defaultHttpClient, + okHttpClients.defaultHttpClientWithoutProxy, + connManager.status, + ) { torSettings, torConnection, clearConnection, connectivity -> + torSettings.hashCode() + torConnection.hashCode() + clearConnection.hashCode() + connectivity.hashCode() + }.debounce(100).onEach { + Log.d("ManageRelayServices", "Relay Services have changed") + client.reconnect(true) + }.onStart { + Log.d("ManageRelayServices", "Resuming Relay Services") + client.connect() + }.onCompletion { + Log.d("ManageRelayServices", "Pausing Relay Services") + client.disconnect() + }.flowOn(Dispatchers.IO) + .stateIn( + scope, + SharingStarted.WhileSubscribed(30000), + null, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt index d74be74973..a4faa10c5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt @@ -22,9 +22,12 @@ package com.vitorpamplona.amethyst.service.relayClient import android.util.Log import com.vitorpamplona.amethyst.service.relayClient.RelaySpeedLogger.Companion.TAG -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.utils.LargeCache import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong @@ -36,7 +39,7 @@ class KindGroup( var count: AtomicInteger = AtomicInteger(0), var memory: AtomicLong = AtomicLong(0L), val subs: LargeCache = LargeCache(), - val relays: LargeCache = LargeCache(), + val relays: LargeCache = LargeCache(), ) { companion object { const val MB: Long = 1024 @@ -45,7 +48,7 @@ class KindGroup( fun increment( mem: Long, subId: String, - relayUrl: String, + relayUrl: NormalizedRelayUrl, ) { count.incrementAndGet() memory.addAndGet(mem) @@ -74,7 +77,7 @@ class KindGroup( fun printSubs() = subs.joinToString(", ") { key, value -> if (value.get() > 0) "$key ($value)" else "" } - fun printRelays() = relays.joinToString(", ") { key, value -> if (value.get() > 0) "${key.removePrefix("wss://").removeSuffix("/")} ($value)" else "" } + fun printRelays() = relays.joinToString(", ") { key, value -> if (value.get() > 0) "${key.displayUrl()} ($value)" else "" } override fun toString() = "(${count.get()} - ${memory.get().div(MB)}kb); ${printSubs()}; ${printRelays()}" } @@ -86,7 +89,7 @@ class FrameStat { fun increment( kind: Int, subId: String, - relayUrl: String, + relayUrl: NormalizedRelayUrl, memory: Long, ) { eventCount.incrementAndGet() @@ -143,16 +146,16 @@ class RelaySpeedLogger( var current = FrameStat() private val clientListener = - object : NostrClient.Listener { + object : IRelayClientListener { /** A new message was received */ override fun onEvent( + relay: IRelayClient, + subId: String, event: Event, - subscriptionId: String, - relay: Relay, arrivalTime: Long, afterEOSE: Boolean, ) { - current.increment(event.kind, subscriptionId, relay.url, event.countMemory()) + current.increment(event.kind, subId, relay.url, event.countMemory()) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt index d0d56d6766..078a2cce93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.relayClient.authCommand.model import android.util.Log import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.datasources.RelayAuthenticator +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.RelayAuthenticator class ScreenAuthAccount( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt index f9389cb07a..236d469ee9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt @@ -59,8 +59,4 @@ abstract class ComposeSubscriptionManager : ComposeSubscriptionManagerControl } fun allKeys() = composeSubscriptions.keys - - fun forEachSubscriber(action: (T) -> Unit) { - composeSubscriptions.keys.forEach(action) - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt index 729864eee2..fb5fce6461 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import android.util.Log import com.vitorpamplona.amethyst.isDebug -import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.ammolite.relays.datasources.SubscriptionController +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient abstract class BaseEoseManager( val client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt index d31b5a7f70..b4ce748ef5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -21,10 +21,11 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.service.relays.EOSEFollowList -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlin.collections.distinctBy /** @@ -50,7 +51,7 @@ abstract class PerUniqueIdEoseManager( fun newEose( key: T, - relayUrl: String, + relayUrl: NormalizedRelayUrl, time: Long, ) { latestEOSEs.newEose(id(key), relayUrl, time) @@ -73,7 +74,7 @@ abstract class PerUniqueIdEoseManager( fun findOrCreateSubFor(key: T): Subscription { val id = id(key) - var subId = userSubscriptionMap[id] + val subId = userSubscriptionMap[id] return if (subId == null) { newSub(key).also { userSubscriptionMap[id] = it.id } } else { @@ -88,7 +89,7 @@ abstract class PerUniqueIdEoseManager( uniqueSubscribedAccounts.forEach { val mainKey = id(it) - findOrCreateSubFor(it).typedFilters = updateFilter(it, since(it))?.ifEmpty { null } + findOrCreateSubFor(it).relayBasedFilters = updateFilter(it, since(it))?.ifEmpty { null } updated.add(mainKey) } @@ -102,8 +103,8 @@ abstract class PerUniqueIdEoseManager( abstract fun updateFilter( key: T, - since: Map?, - ): List? + since: SincePerRelayMap?, + ): List? abstract fun id(key: T): String } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index b02d63f987..62e130b8b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -22,10 +22,11 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccount -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl /** * This query type creates a new relay subscription for every logged-in @@ -50,9 +51,9 @@ abstract class PerUserAndFollowListEoseManager( fun newEose( key: T, - relayUrl: String, + relay: NormalizedRelayUrl, time: Long, - ) = latestEOSEs.newEose(user(key), list(key), relayUrl, time) + ) = latestEOSEs.newEose(user(key), list(key), relay, time) open fun newSub(key: T): Subscription = orchestrator.requestNewSubscription { time, relayUrl -> @@ -71,7 +72,7 @@ abstract class PerUserAndFollowListEoseManager( fun findOrCreateSubFor(key: T): Subscription { val user = user(key) - var subId = userSubscriptionMap[user] + val subId = userSubscriptionMap[user] return if (subId == null) { newSub(key).also { userSubscriptionMap[user] = it.id } } else { @@ -86,7 +87,7 @@ abstract class PerUserAndFollowListEoseManager( uniqueSubscribedAccounts.forEach { val user = user(it) - findOrCreateSubFor(it).typedFilters = updateFilter(it, since(it))?.ifEmpty { null } + findOrCreateSubFor(it).relayBasedFilters = updateFilter(it, since(it))?.ifEmpty { null } updated.add(user) } @@ -100,8 +101,8 @@ abstract class PerUserAndFollowListEoseManager( abstract fun updateFilter( key: T, - since: Map?, - ): List? + since: SincePerRelayMap?, + ): List? abstract fun user(key: T): User diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index 1bbe13acd3..8f06ae1ee7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -22,10 +22,11 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlin.collections.distinctBy /** @@ -49,9 +50,9 @@ abstract class PerUserEoseManager( fun newEose( key: T, - relayUrl: String, + relay: NormalizedRelayUrl, time: Long, - ) = latestEOSEs.newEose(user(key), relayUrl, time) + ) = latestEOSEs.newEose(user(key), relay, time) open fun newSub(key: T): Subscription = orchestrator.requestNewSubscription { time, relayUrl -> @@ -70,7 +71,7 @@ abstract class PerUserEoseManager( fun findOrCreateSubFor(key: T): Subscription { val user = user(key) - var subId = userSubscriptionMap[user] + val subId = userSubscriptionMap[user] return if (subId == null) { newSub(key).also { userSubscriptionMap[user] = it.id } } else { @@ -85,7 +86,7 @@ abstract class PerUserEoseManager( uniqueSubscribedAccounts.forEach { val user = user(it) - findOrCreateSubFor(it).typedFilters = updateFilter(it, since(it))?.ifEmpty { null } + findOrCreateSubFor(it).relayBasedFilters = updateFilter(it, since(it))?.ifEmpty { null } updated.add(user) } @@ -99,8 +100,8 @@ abstract class PerUserEoseManager( abstract fun updateFilter( key: T, - since: Map?, - ): List? + since: SincePerRelayMap?, + ): List? abstract fun user(key: T): User } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt index 10c5eb3b01..992465e0cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt @@ -21,9 +21,10 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.service.relays.EOSERelayList -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlin.collections.distinctBy /** @@ -49,9 +50,9 @@ abstract class SingleSubEoseManager( fun since() = latestEOSEs.since() open fun newEose( - relayUrl: String, + relay: NormalizedRelayUrl, time: Long, - ) = latestEOSEs.newEose(relayUrl, time) + ) = latestEOSEs.newEose(relay, time) val sub = orchestrator.requestNewSubscription { time, relayUrl -> @@ -64,13 +65,13 @@ abstract class SingleSubEoseManager( override fun updateSubscriptions(keys: Set) { val uniqueSubscribedAccounts = keys.distinctBy { distinct(it) } - sub.typedFilters = updateFilter(uniqueSubscribedAccounts, since())?.ifEmpty { null } + sub.relayBasedFilters = updateFilter(uniqueSubscribedAccounts, since())?.ifEmpty { null } } abstract fun updateFilter( - key: List, - since: Map?, - ): List? + keys: List, + since: SincePerRelayMap?, + ): List? abstract fun distinct(key: T): Any } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt index f897011628..86a72b96d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import kotlin.collections.distinctBy /** @@ -44,10 +44,10 @@ abstract class SingleSubNoEoseCacheEoseManager( override fun updateSubscriptions(keys: Set) { val uniqueSubscribedAccounts = keys.distinctBy { distinct(it) } - sub.typedFilters = updateFilter(uniqueSubscribedAccounts)?.ifEmpty { null } + sub.relayBasedFilters = updateFilter(uniqueSubscribedAccounts)?.ifEmpty { null } } - abstract fun updateFilter(key: List): List? + abstract fun updateFilter(keys: List): List? abstract fun distinct(key: T): Any } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt index 921369e0c3..cbcdcb88eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R @@ -28,7 +29,8 @@ import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.Notify import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl +import kotlinx.coroutines.flow.map @Composable fun DisplayNotifyMessages( @@ -42,14 +44,21 @@ fun DisplayNotifyMessages( accountViewModel: AccountViewModel, nav: INav, ) { - val openDialogMsg = requests.transientPaymentRequests.collectAsStateWithLifecycle(emptySet()) + val flow = + remember(accountViewModel) { + requests.transientPaymentRequests.map { + it.filter { it.relayUrl in accountViewModel.account.trustedRelays.flow.value } + } + } + + val openDialogMsg = flow.collectAsStateWithLifecycle(emptySet()) openDialogMsg.value.firstOrNull()?.let { request -> NotifyRequestDialog( title = stringRes( id = R.string.payment_required_title, - RelayUrlFormatter.displayUrl(request.relayUrl), + request.relayUrl.displayUrl(), ), textContent = request.description, accountViewModel = accountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt index 1caa586d57..90d3927374 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.datasources.RelayNotifier +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.RelayNotifier class NotifyCoordinator( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyRequest.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyRequest.kt index 1d0f35e812..74aefabfac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyRequest.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyRequest.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + data class NotifyRequest( - val relayUrl: String, + val relayUrl: NormalizedRelayUrl, val description: String, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyRequestsCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyRequestsCache.kt index a908f54a72..2e7fccc18b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyRequestsCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyRequestsCache.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -29,9 +30,9 @@ class NotifyRequestsCache { fun addPaymentRequestIfNew( description: String, - relayUrl: String, + relayUrl: NormalizedRelayUrl, ) { - addPaymentRequestIfNew(NotifyRequest(description, relayUrl)) + addPaymentRequestIfNew(NotifyRequest(relayUrl, description)) } fun addPaymentRequestIfNew(paymentRequest: NotifyRequest) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt index 9db47b5aac..fb0c247746 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterA import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource.UserProfileFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssembler -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import kotlinx.coroutines.CoroutineScope class RelaySubscriptionsCoordinator( @@ -63,7 +63,7 @@ class RelaySubscriptionsCoordinator( val userFinder = UserFinderFilterAssembler(client) // active when searching or tagging users. - val search = SearchFilterAssembler(cache, client, scope) + val search = SearchFilterAssembler(client, scope, cache) // active depending on the screen. val channel = ChannelFilterAssembler(client) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt index a316a6939c..cd4be9b40f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata.AccountMetadataEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps.AccountGiftWrapsEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to logged-in accounts. class AccountQueryState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt index 44b8cf6994..4c201bcce7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt @@ -37,7 +37,7 @@ fun AccountFilterAssemblerSubscription( // even if they are tracking the same tag. val state = remember(accountViewModel) { - AccountQueryState(accountViewModel.account, accountViewModel.allAccountsSync().toSet()) + AccountQueryState(accountViewModel.account, accountViewModel.trustedAccounts.value) } KeyDataSourceSubscription(state, dataSource) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt index a9529ebcc2..d247179647 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountObservers.kt @@ -37,7 +37,7 @@ fun observeAccountIsHiddenWord( // Subscribe in the LocalCache for changes that arrive in the device val flow = remember(account, word) { - account.flowHiddenUsers + account.hiddenUsers.flow .map { word in it.hiddenWords } .distinctUntilChanged() } @@ -53,7 +53,7 @@ fun observeAccountIsHiddenUser( // Subscribe in the LocalCache for changes that arrive in the device val flow = remember(account, user) { - account.flowHiddenUsers + account.hiddenUsers.flow .map { it.hiddenUsers.contains(user.pubkeyHex) || it.spammers.contains(user.pubkeyHex) } .distinctUntilChanged() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt index 26fcb1afb7..61f9a7b7b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt @@ -20,11 +20,19 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlin.collections.forEach class AccountMetadataEoseManager( client: NostrClient, @@ -32,15 +40,46 @@ class AccountMetadataEoseManager( ) : PerUserEoseManager(client, allKeys) { override fun user(query: AccountQueryState) = query.account.userProfile() + fun relayFlow(query: AccountQueryState) = query.account.outboxRelays.flow + override fun updateFilter( key: AccountQueryState, - since: Map?, - ): List? = - listOfNotNull( - filterAccountInfoAndListsFromKey(user(key).pubkeyHex, since), - filterFollowsAndMutesFromKey(user(key).pubkeyHex, since), - filterDraftsAndReportsFromKey(user(key).pubkeyHex, since), - filterLastPostsFromKey(user(key).pubkeyHex, since), - filterBasicAccountInfoFromKeys(key.otherAccounts.minus(key.account.userProfile().pubkeyHex).toList(), since), - ).flatten() + since: SincePerRelayMap?, + ): List = + relayFlow(key).value.flatMap { + val since = since?.get(it)?.time + listOf( + filterAccountInfoAndListsFromKey(it, user(key).pubkeyHex, since), + filterFollowsAndMutesFromKey(it, user(key).pubkeyHex, since), + filterDraftsAndReportsFromKey(it, user(key).pubkeyHex, since), + filterLastPostsFromKey(it, user(key).pubkeyHex, since), + filterBasicAccountInfoFromKeys(it, key.otherAccounts.minus(key.account.userProfile().pubkeyHex).toList(), since), + ).flatten() + } + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: AccountQueryState): Subscription { + val user = user(key) + userJobMap[user]?.forEach { it.cancel() } + userJobMap[user] = + listOf( + key.account.scope.launch(Dispatchers.Default) { + relayFlow(key).collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 0eacefe3ff..8909f2b978 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -20,15 +20,13 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.blossom.BlossomServersEvent +import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState.Companion.APP_SPECIFIC_DATA_D_TAG import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent @@ -36,42 +34,49 @@ import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent + +val AccountInfoAndListsFromKeyKinds = + listOf( + MetadataEvent.KIND, + ContactListEvent.KIND, + StatusEvent.KIND, + AdvertisedRelayListEvent.KIND, + ChatMessageRelayListEvent.KIND, + SearchRelayListEvent.KIND, + FileServersEvent.KIND, + BlossomServersEvent.KIND, + PrivateOutboxRelayListEvent.KIND, + ) + +val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND) +val AmethystMetadataTagMapFilter = mapOf("d" to listOf(APP_SPECIFIC_DATA_D_TAG)) fun filterAccountInfoAndListsFromKey( - pubkey: HexKey?, - since: Map?, -): List? { - if (pubkey == null || pubkey.isEmpty()) return null + relay: NormalizedRelayUrl, + pubkey: HexKey, + since: Long?, +): List { + if (pubkey.isEmpty()) return emptyList() return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - MetadataEvent.KIND, - ContactListEvent.KIND, - StatusEvent.KIND, - AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, - SearchRelayListEvent.KIND, - FileServersEvent.KIND, - BlossomServersEvent.KIND, - PrivateOutboxRelayListEvent.KIND, - ), + Filter( + kinds = AccountInfoAndListsFromKeyKinds, authors = listOf(pubkey), limit = 20, since = since, ), ), - TypedFilter( - types = COMMON_FEED_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = listOf(AppSpecificDataEvent.KIND), + Filter( + kinds = AmethystMetadataKinds, authors = listOf(pubkey), - tags = mapOf("d" to listOf(Account.APP_SPECIFIC_DATA_D_TAG)), + tags = AmethystMetadataTagMapFilter, limit = 1, since = since, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt index d25ae69745..3ab934bd29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -20,40 +20,42 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.blossom.BlossomServersEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent + +val BasicAccountInfoKinds = + listOf( + MetadataEvent.KIND, + ContactListEvent.KIND, + AdvertisedRelayListEvent.KIND, + ChatMessageRelayListEvent.KIND, + SearchRelayListEvent.KIND, + FileServersEvent.KIND, + BlossomServersEvent.KIND, + ) fun filterBasicAccountInfoFromKeys( + relay: NormalizedRelayUrl, otherAccounts: List?, - since: Map?, -): List? { - if (otherAccounts == null || otherAccounts.isEmpty()) return null + since: Long?, +): List { + if (otherAccounts == null || otherAccounts.isEmpty()) return emptyList() return listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - MetadataEvent.KIND, - ContactListEvent.KIND, - AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, - SearchRelayListEvent.KIND, - FileServersEvent.KIND, - BlossomServersEvent.KIND, - ), + Filter( + kinds = BasicAccountInfoKinds, authors = otherAccounts.toList(), limit = otherAccounts.size * 8, since = since, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt index b480853b34..0416ae94c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt @@ -20,32 +20,34 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent +val DraftsAndReportsFromKeyKinds = + listOf( + DraftEvent.KIND, + ReportEvent.KIND, + BookmarkListEvent.KIND, + ) + fun filterDraftsAndReportsFromKey( + relay: NormalizedRelayUrl, pubkey: HexKey?, - since: Map?, -): List? { - if (pubkey == null || pubkey.isEmpty()) return null + since: Long?, +): List { + if (pubkey == null || pubkey.isEmpty()) return emptyList() return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - DraftEvent.KIND, - ReportEvent.KIND, - BookmarkListEvent.KIND, - ), + Filter( + kinds = DraftsAndReportsFromKeyKinds, authors = listOf(pubkey), since = since, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt index 78eb060517..8f17ce115a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt @@ -20,12 +20,11 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent @@ -33,27 +32,30 @@ import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent +val FollowAndMutesFromKeyKinds = + listOf( + PeopleListEvent.KIND, + FollowListEvent.KIND, + MuteListEvent.KIND, + BadgeProfilesEvent.KIND, + EmojiPackSelectionEvent.KIND, + EphemeralChatListEvent.KIND, + ChannelListEvent.KIND, + ) + fun filterFollowsAndMutesFromKey( - pubkey: HexKey?, - since: Map?, -): List? { - if (pubkey == null || pubkey.isEmpty()) return null + relay: NormalizedRelayUrl, + pubkey: HexKey, + since: Long?, +): List { + if (pubkey.isEmpty()) return emptyList() return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - PeopleListEvent.KIND, - FollowListEvent.KIND, - MuteListEvent.KIND, - BadgeProfilesEvent.KIND, - EmojiPackSelectionEvent.KIND, - EphemeralChatListEvent.KIND, - ChannelListEvent.KIND, - ), + Filter( + kinds = FollowAndMutesFromKeyKinds, authors = listOf(pubkey), limit = 100, since = since, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt index 6ff9941925..00b0ec1eeb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterLastPostsFromKey.kt @@ -20,23 +20,23 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl fun filterLastPostsFromKey( - pubkey: HexKey?, - since: Map?, -): List? { - if (pubkey == null || pubkey.isEmpty()) return null + relay: NormalizedRelayUrl, + pubkey: HexKey, + since: Long?, +): List { + if (pubkey.isEmpty()) return emptyList() return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( + Filter( authors = listOf(pubkey), limit = 100, since = since, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt index 11d28a4377..efa2107a91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt @@ -20,11 +20,19 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlin.collections.forEach class AccountNotificationsEoseManager( client: NostrClient, @@ -34,6 +42,35 @@ class AccountNotificationsEoseManager( override fun updateFilter( key: AccountQueryState, - since: Map?, - ): List? = filterNotificationsToPubkey(user(key).pubkeyHex, since) + since: SincePerRelayMap?, + ): List? = + key.account.notificationRelays.flow.value.flatMap { + filterNotificationsToPubkey(it, user(key).pubkeyHex, since?.get(it)?.time) + } + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: AccountQueryState): Subscription { + val user = user(key) + userJobMap[user]?.forEach { it.cancel() } + userJobMap[user] = + listOf( + key.account.scope.launch(Dispatchers.Default) { + key.account.notificationRelays.flow.collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt index 6f53eb51b3..8a7ba5e3ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt @@ -20,15 +20,14 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -47,62 +46,69 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +val NotificationsPerKeyKinds = + listOf( + TextNoteEvent.KIND, + ReactionEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + ReportEvent.KIND, + LnZapEvent.KIND, + LnZapPaymentResponseEvent.KIND, + ChannelMessageEvent.KIND, + EphemeralChatEvent.KIND, + BadgeAwardEvent.KIND, + ) + +val NotificationsPerKeyKinds2 = + listOf( + GitReplyEvent.KIND, + GitIssueEvent.KIND, + GitPatchEvent.KIND, + HighlightEvent.KIND, + CommentEvent.KIND, + CalendarDateSlotEvent.KIND, + CalendarTimeSlotEvent.KIND, + CalendarRSVPEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + InteractiveStorySceneEvent.KIND, + ) + +val NotificationsPerKeyKinds3 = listOf(PollNoteEvent.KIND) + fun filterNotificationsToPubkey( + relay: NormalizedRelayUrl, pubkey: HexKey?, - since: Map?, -): List? { - if (pubkey == null || pubkey.isEmpty()) return null + since: Long?, +): List { + if (pubkey == null || pubkey.isEmpty()) return emptyList() return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - ReactionEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - ReportEvent.KIND, - LnZapEvent.KIND, - LnZapPaymentResponseEvent.KIND, - ChannelMessageEvent.KIND, - EphemeralChatEvent.KIND, - BadgeAwardEvent.KIND, - ), + Filter( + kinds = NotificationsPerKeyKinds, tags = mapOf("p" to listOf(pubkey)), limit = 2000, since = since, ), ), - TypedFilter( - types = COMMON_FEED_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - GitReplyEvent.KIND, - GitIssueEvent.KIND, - GitPatchEvent.KIND, - HighlightEvent.KIND, - CommentEvent.KIND, - CalendarDateSlotEvent.KIND, - CalendarTimeSlotEvent.KIND, - CalendarRSVPEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - InteractiveStorySceneEvent.KIND, - ), + Filter( + kinds = NotificationsPerKeyKinds2, tags = mapOf("p" to listOf(pubkey)), limit = 400, since = since, ), ), - TypedFilter( - types = COMMON_FEED_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = listOf(PollNoteEvent.KIND), + Filter( + kinds = NotificationsPerKeyKinds3, tags = mapOf("p" to listOf(pubkey)), limit = 100, since = since, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt index 1965272820..3bcc0e0390 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt @@ -20,11 +20,19 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlin.collections.forEach class AccountGiftWrapsEoseManager( client: NostrClient, @@ -34,6 +42,40 @@ class AccountGiftWrapsEoseManager( override fun updateFilter( key: AccountQueryState, - since: Map?, - ): List? = filterGiftWrapsToPubkey(user(key).pubkeyHex, since) + since: SincePerRelayMap?, + ): List { + return key.account.dmRelays.flow.value.flatMap { relay -> + filterGiftWrapsToPubkey( + relay = relay, + pubkey = user(key).pubkeyHex, + since = since?.get(relay)?.time, + ) + } + } + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: AccountQueryState): Subscription { + val user = user(key) + userJobMap[user]?.forEach { it.cancel() } + userJobMap[user] = + listOf( + key.account.scope.launch(Dispatchers.Default) { + key.account.dmRelays.flow.collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt index 2ea5f5057b..a9b0765a22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/FilterGiftWrapsToPubkey.kt @@ -20,31 +20,28 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.TimeUtils fun filterGiftWrapsToPubkey( + relay: NormalizedRelayUrl, pubkey: HexKey?, - since: Map?, -): List? { - if (pubkey == null || pubkey.isEmpty()) return null + since: Long?, +): List { + if (pubkey == null || pubkey.isEmpty()) return emptyList() return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( + Filter( kinds = listOf(GiftWrapEvent.KIND), tags = mapOf("p" to listOf(pubkey)), - since = - since?.mapValues { - EOSETime(it.value.time - TimeUtils.twoDays()) - }, + since = since?.minus(TimeUtils.twoDays()), ), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt index 348d101fd7..cc794358fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive.ChannelMetadataAndLiveActivityWatcherSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.ChannelLoaderSubAssembler -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to tags, even the same tag class ChannelFinderQueryState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index cfd22c6114..72a604220b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -73,10 +73,12 @@ fun observeChannelNoteAuthors( .toSet() .toImmutableList() }.onStart { - baseChannel.notes - .mapNotNull { key, value -> value.author } - .toSet() - .toImmutableList() + emit( + baseChannel.notes + .mapNotNull { key, value -> value.author } + .toSet() + .toImmutableList(), + ) }.distinctUntilChanged() .flowOn(Dispatchers.Default) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt index efd139f458..b8cb33d36a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt @@ -26,9 +26,10 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEose import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.filterChannelMetadataUpdatesById import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities.filterLiveStreamUpdatesByAddress -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.utils.mapOfSet /** * This assembler observes modifications to the LiveActivity root events @@ -43,18 +44,37 @@ class ChannelMetadataAndLiveActivityWatcherSubAssembler( ) : SingleSubEoseManager(client, allKeys) { override fun updateFilter( keys: List, - since: Map?, - ): List? = - keys - .mapNotNull { key -> - if (key.channel is PublicChatChannel) { - filterChannelMetadataUpdatesById(key.channel, since) - } else if (key.channel is LiveActivitiesChannel) { - filterLiveStreamUpdatesByAddress(key.channel, since) - } else { - null + since: SincePerRelayMap?, + ): List { + val perRelayPublicChannelFilter = + mapOfSet { + keys.forEach { key -> + if (key.channel is PublicChatChannel) { + key.channel.relays().forEach { + add(it, key.channel) + } + } } - }.flatten() + } + + val perRelayLiveActivityFilter = + mapOfSet { + keys.forEach { key -> + if (key.channel is LiveActivitiesChannel) { + key.channel.relays().forEach { + add(it, key.channel) + } + } + } + } + + return perRelayPublicChannelFilter.flatMap { (relay, channels) -> + filterChannelMetadataUpdatesById(relay, channels.toList(), since?.get(relay)?.time) + } + + perRelayLiveActivityFilter.flatMap { (relay, channels) -> + filterLiveStreamUpdatesByAddress(relay, channels.toList(), since?.get(relay)?.time) + } + } override fun distinct(key: ChannelFinderQueryState) = key.channel.idHex } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt index 399338b81c..83ea7d3dee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28P import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter /** * This assembler observes loads missing public chats when needed. @@ -38,7 +38,7 @@ class ChannelLoaderSubAssembler( client: NostrClient, allKeys: () -> Set, ) : SingleSubNoEoseCacheEoseManager(client, allKeys, invalidateAfterEose = true) { - override fun updateFilter(keys: List): List? = filterMissingChannelsById(keys) + override fun updateFilter(keys: List): List? = filterMissingChannelsById(keys) override fun distinct(key: ChannelFinderQueryState) = key.channel.idHex } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt index 2b7519be9e..b515fba131 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28P import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ChannelMetadataWatcherSubAssembler( client: NostrClient, @@ -33,12 +33,14 @@ class ChannelMetadataWatcherSubAssembler( ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: ChannelFinderQueryState, - since: Map?, - ): List? = + since: SincePerRelayMap?, + ): List = if (key.channel is PublicChatChannel) { - filterChannelMetadataUpdatesById(key.channel, since) + key.channel.relays().flatMap { + filterChannelMetadataUpdatesById(it, listOf(key.channel), since?.get(it)?.time) + } } else { - null + emptyList() } /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt index 8613411913..26d82439ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt @@ -22,31 +22,41 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28P import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.utils.mapOfSet -fun filterMissingChannelsById(keys: List): List? { - val firstTimers = - keys.mapNotNullTo(mutableSetOf()) { - if (it.channel is PublicChatChannel && it.channel.event == null) { - it.channel.idHex - } else { - null +val filterMissingPublicChannelsByIdKinds = listOf(ChannelCreateEvent.KIND) + +fun filterMissingChannelsById(keys: List): List { + val relayPerChannel = + mapOfSet { + keys.forEach { key -> + if (key.channel is PublicChatChannel && key.channel.event == null) { + key.channel.relays().forEach { + add(it, key.channel.idHex) + } + } else { + null + } } } - if (firstTimers.isEmpty()) return null + if (relayPerChannel.isEmpty()) return emptyList() - return listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(ChannelCreateEvent.KIND), - ids = firstTimers.toList(), - ), - ), - ) + return relayPerChannel.mapNotNull { + if (it.value.isEmpty()) { + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = filterMissingPublicChannelsByIdKinds, + ids = it.value.sorted(), + ), + ) + } else { + null + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt index 518f44b994..067e3c9b2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt @@ -21,23 +21,25 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +val channelMetadataKinds = listOf(ChannelMetadataEvent.KIND) + fun filterChannelMetadataUpdatesById( - channel: PublicChatChannel, - since: Map?, -): List = + relay: NormalizedRelayUrl, + channels: List, + since: Long?, +): List = listOf( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = listOf(ChannelMetadataEvent.KIND), - tags = mapOf("e" to listOf(channel.idHex)), + Filter( + kinds = channelMetadataKinds, + tags = mapOf("e" to channels.map { it.idHex }), since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt index c0a77a9a35..52f87f347c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt @@ -21,24 +21,25 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent fun filterLiveStreamUpdatesByAddress( - channel: LiveActivitiesChannel, - since: Map?, -): List { - val stream = channel.address() + relay: NormalizedRelayUrl, + channels: List, + since: Long?, +): List { + // this runs a cross product of all ds and for pubkeys, but we are assuming they are quite unique. return listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = listOf(stream.kind), - tags = mapOf("d" to listOf(stream.dTag)), - authors = listOf(stream.pubKeyHex), + Filter( + kinds = listOf(LiveActivitiesEvent.KIND), + tags = mapOf("d" to channels.map { it.address.dTag }), + authors = channels.map { it.address.pubKeyHex }, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt index 7a7a677ef8..6aa98492bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53L import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter /** * This assembler observes modifications to the LiveActivity root events @@ -37,13 +37,16 @@ class LiveActivityWatcherSubAssembly( ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: ChannelFinderQueryState, - since: Map?, - ): List? = - if (key.channel is LiveActivitiesChannel) { - filterLiveStreamUpdatesByAddress(key.channel, since) + since: SincePerRelayMap?, + ): List { + return if (key.channel is LiveActivitiesChannel) { + key.channel.relays().flatMap { + filterLiveStreamUpdatesByAddress(it, listOf(key.channel), since?.get(it)?.time) + } } else { - null + emptyList() } + } /** * Only one key per channel. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt index 2176a1c349..0ba866dcfd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers.EventWatcherSubAssembler -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to tags, even the same tag class EventFinderQueryState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt index fd0fb282c6..c29bdaeb40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt @@ -21,13 +21,14 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -fun filterMissingAddressables(keys: List): List? { +fun filterMissingAddressables(keys: List): List? { val missingAddressables = mutableSetOf
() keys.forEach { @@ -46,31 +47,43 @@ fun filterMissingAddressables(keys: List): List): List? { - if (missingAddressables.isEmpty()) return null +fun filterMissingAddressables(missingAddressables: Set
): List { + if (missingAddressables.isEmpty()) return emptyList() - return missingAddressables.map { aTag -> - if (aTag.kind < 25000 && aTag.dTag.isBlank()) { - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(aTag.kind), - authors = listOf(aTag.pubKeyHex), - limit = 1, - ), - ) - } else { - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(aTag.kind), - tags = mapOf("d" to listOf(aTag.dTag)), - authors = listOf(aTag.pubKeyHex), - limit = 1, - ), - ) + return missingAddressables.flatMap { aTag -> + val authorHomeRelayEventAddress = AdvertisedRelayListEvent.createAddressTag(aTag.pubKeyHex) + val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) + + val authorHomeRelays = + authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null } + ?: LocalCache.relayHints.hintsForKey(aTag.pubKeyHex).ifEmpty { null } + ?: listOfNotNull(LocalCache.getUserIfExists(aTag.pubKeyHex)?.latestMetadataRelay) + + val relayHints = LocalCache.relayHints.hintsForAddress(aTag.toValue()) + + (authorHomeRelays + relayHints).toSet().map { + if (aTag.kind < 25000 && aTag.dTag.isBlank()) { + RelayBasedFilter( + relay = it, + filter = + Filter( + kinds = listOf(aTag.kind), + authors = listOf(aTag.pubKeyHex), + limit = 1, + ), + ) + } else { + RelayBasedFilter( + relay = it, + filter = + Filter( + kinds = listOf(aTag.kind), + tags = mapOf("d" to listOf(aTag.dTag)), + authors = listOf(aTag.pubKeyHex), + limit = 1, + ), + ) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt index 9fb917c1d5..fb8cc330ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt @@ -21,13 +21,15 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.MapOfSetBuilder -fun filterMissingEvents(keys: List): List? { +fun filterMissingEvents(keys: List): List? { val missingEvents = mutableSetOf() keys.forEach { @@ -46,13 +48,21 @@ fun filterMissingEvents(keys: List): List? { return filterMissingEvents(missingEvents) } -fun filterMissingEvents(missingEventIds: Set): List? { - if (missingEventIds.isEmpty()) return null +fun filterMissingEvents(missingEventIds: Set): List { + if (missingEventIds.isEmpty()) return emptyList() - return listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = SincePerRelayFilter(ids = missingEventIds.sorted()), - ), - ) + val relayHints = MapOfSetBuilder() + + missingEventIds.forEach { eventId -> + LocalCache.relayHints.hintsForEvent(eventId).forEach { relayUrl -> + relayHints.add(relayUrl, eventId) + } + } + + return relayHints.build().map { + RelayBasedFilter( + relay = it.key, + filter = Filter(ids = it.value.sorted()), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt index 2a64971696..715a6f48eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/NoteEventLoaderSubAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient class NoteEventLoaderSubAssembler( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt index a9ff3eb0e4..483ff830bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt @@ -25,9 +25,11 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.ammolite.relays.filters.MutableTime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class EventWatcherSubAssembler( client: NostrClient, @@ -37,24 +39,24 @@ class EventWatcherSubAssembler( var latestEOSEs: EOSEAccountFast = EOSEAccountFast(10000) override fun newEose( - relayUrl: String, + relay: NormalizedRelayUrl, time: Long, ) { lastNotesOnFilter.forEach { - latestEOSEs.newEose(it, relayUrl, time) + latestEOSEs.newEose(it, relay, time) } - super.newEose(relayUrl, time) + super.newEose(relay, time) } override fun updateFilter( - keys: List, - since: Map?, - ): List? { - if (keys.isEmpty()) { + key: List, + since: SincePerRelayMap?, + ): List? { + if (key.isEmpty()) { return null } - lastNotesOnFilter = keys.map { it.note } + lastNotesOnFilter = key.map { it.note } return groupByRelayPresence(lastNotesOnFilter, latestEOSEs) .map { group -> @@ -65,7 +67,6 @@ class EventWatcherSubAssembler( listOfNotNull( filterRepliesAndReactionsToNotes(events, findMinimumEOSEs(events, latestEOSEs)), filterRepliesAndReactionsToAddresses(addressables, findMinimumEOSEs(addressables, latestEOSEs)), - filterQuotesToNotes(group, findMinimumEOSEs(group, latestEOSEs)), ).flatten() } else { emptyList() @@ -88,16 +89,16 @@ class EventWatcherSubAssembler( fun findMinimumEOSEs( notes: List, eoseCache: EOSEAccountFast, - ): Map { - val minLatestEOSEs = mutableMapOf() + ): SincePerRelayMap { + val minLatestEOSEs = mutableMapOf() notes.forEach { note -> eoseCache.since(note)?.forEach { val minEose = minLatestEOSEs[it.key] if (minEose == null) { - minLatestEOSEs.put(it.key, EOSETime(it.value.time)) - } else if (it.value.time < minEose.time) { - minEose.time = it.value.time + minLatestEOSEs.put(it.key, it.value.copy()) + } else { + minEose.updateIfOlder(it.value.time) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt index 244d613c20..f062f2982a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt @@ -21,11 +21,10 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent @@ -35,51 +34,80 @@ import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessa import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.mapOfSet + +val RepliesAndReactiionsToAddressesKinds1 = + listOf( + TextNoteEvent.KIND, + ReactionEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + ReportEvent.KIND, + LnZapEvent.KIND, + PollNoteEvent.KIND, + CommunityPostApprovalEvent.KIND, + LiveActivitiesChatMessageEvent.KIND, + ) + +val DeletionKindList = + listOf( + DeletionEvent.KIND, + ) + +val TextNoteKindList = listOf(TextNoteEvent.KIND) fun filterRepliesAndReactionsToAddresses( keys: List, - since: Map?, -): List? { + since: SincePerRelayMap?, +): List? { if (keys.isEmpty()) return null - val addresses = keys.mapTo(mutableSetOf()) { it.address().toValue() }.sorted() + val notesPerRelay = + mapOfSet { + keys.forEach { + it.relayUrlsForReactions().forEach { relay -> + add(relay, it.address().toValue()) + } + } + } - return listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - ReactionEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - ReportEvent.KIND, - LnZapEvent.KIND, - PollNoteEvent.KIND, - CommunityPostApprovalEvent.KIND, - LiveActivitiesChatMessageEvent.KIND, - ), - tags = mapOf("a" to addresses), - since = since, - // Max amount of "replies" to download on a specific event. - limit = 1000, - ), - ), - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - DeletionEvent.KIND, - ), - tags = mapOf("a" to addresses), - since = since, - // Max amount of "replies" to download on a specific event. - limit = 10, - ), - ), - ) + return notesPerRelay.flatMap { + val since = since?.get(it.key)?.time + val sortedList = it.value.sorted() + val relay = it.key + listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = RepliesAndReactiionsToAddressesKinds1, + tags = mapOf("a" to sortedList), + since = since, + // Max amount of "replies" to download on a specific event. + limit = 1000, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = DeletionKindList, + tags = mapOf("a" to sortedList), + since = since, + // Max amount of "replies" to download on a specific event. + limit = 10, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = TextNoteKindList, + tags = mapOf("q" to sortedList), + since = since, + limit = 1000, + ), + ), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt index b738ae4e0d..f88122b5f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt @@ -21,12 +21,11 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.watchers import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -39,54 +38,81 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent +import com.vitorpamplona.quartz.utils.mapOfSet + +val RepliesAndReactionsKinds = + listOf( + TextNoteEvent.KIND, + ReactionEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + ReportEvent.KIND, + LnZapEvent.KIND, + PollNoteEvent.KIND, + OtsEvent.KIND, + TextNoteModificationEvent.KIND, + GitReplyEvent.KIND, + ) + +val RepliesAndReactionsKinds2 = + listOf( + DeletionEvent.KIND, + NIP90ContentDiscoveryResponseEvent.KIND, + NIP90StatusEvent.KIND, + TorrentCommentEvent.KIND, + ) fun filterRepliesAndReactionsToNotes( events: List, - since: Map?, -): List? { + since: SincePerRelayMap?, +): List? { if (events.isEmpty()) return null - val eventIds = events.mapTo(mutableSetOf()) { it.idHex }.sorted() + val perRelayEventIds = + mapOfSet { + events.forEach { note -> + note.relayUrlsForReactions().forEach { relay -> + add(relay, note.idHex) + } + } + } - return listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - ReactionEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - ReportEvent.KIND, - LnZapEvent.KIND, - PollNoteEvent.KIND, - OtsEvent.KIND, - TextNoteModificationEvent.KIND, - GitReplyEvent.KIND, - ), - tags = mapOf("e" to eventIds), - since = since, - // Max amount of "replies" to download on a specific event. - limit = 1000, - ), - ), - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - DeletionEvent.KIND, - NIP90ContentDiscoveryResponseEvent.KIND, - NIP90StatusEvent.KIND, - TorrentCommentEvent.KIND, - ), - tags = mapOf("e" to eventIds), - since = since, - limit = 100, - ), - ), - ) + return perRelayEventIds.flatMap { + val since = since?.get(it.key)?.time + val sortedList = it.value.sorted() + val relay = it.key + listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = RepliesAndReactionsKinds, + tags = mapOf("e" to sortedList), + since = since, + // Max amount of "replies" to download on a specific event. + limit = 1000, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = RepliesAndReactionsKinds2, + tags = mapOf("e" to sortedList), + since = since, + limit = 100, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(TextNoteEvent.KIND), + tags = mapOf("q" to sortedList), + since = since, + limit = 1000, + ), + ), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/FilterNWCPaymentsFromRequests.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/FilterNWCPaymentsFromRequests.kt index 6a21e0517d..140ca7ca05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/FilterNWCPaymentsFromRequests.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/FilterNWCPaymentsFromRequests.kt @@ -20,31 +20,21 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent fun filterNWCPaymentsFromRequests( serviceKeys: Set, - paymentRequets: Set, + paymentRequests: Set, fromUsers: Set, - since: Map?, -): List = - listOf( - TypedFilter( - types = setOf(FeedType.WALLET_CONNECT), - filter = - SincePerRelayFilter( - kinds = listOf(LnZapPaymentResponseEvent.KIND), - authors = serviceKeys.toList(), - tags = - mapOf( - "e" to paymentRequets.toList(), - "p" to fromUsers.toList(), - ), - ), - ), +): Filter = + Filter( + kinds = listOf(LnZapPaymentResponseEvent.KIND), + authors = serviceKeys.sorted(), + tags = + mapOf( + "e" to paymentRequests.sorted(), + "p" to fromUsers.sorted(), + ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt new file mode 100644 index 0000000000..fbb130e337 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCFinderFilterAssemblerSubscription.kt @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent + +@SuppressLint("StateFlowValueCalledInComposition") +@Composable +fun NWCFinderFilterAssemblerSubscription( + note: Note, + accountViewModel: AccountViewModel, +) = NWCFinderFilterAssemblerSubscription( + note, + accountViewModel.dataSources().nwc, +) + +@Composable +fun NWCFinderFilterAssemblerSubscription( + note: Note, + dataSource: NWCPaymentFilterAssembler, +) { + // different screens get different states + // even if they are tracking the same tag. + val states = + remember(note) { + val zapPaymentRequestNote = note + (zapPaymentRequestNote.event as? LnZapPaymentRequestEvent)?.let { noteEvent -> + noteEvent.walletServicePubKey()?.let { serviceId -> + zapPaymentRequestNote.relays.map { + NWCPaymentQueryState( + fromServiceHex = serviceId, + toUserHex = noteEvent.pubKey, + replyingToHex = noteEvent.id, + relay = it, + ) + } + } + } + } + + states?.forEach { + KeyDataSourceSubscription(it, dataSource) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt index 361036420f..4e4d91572f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt @@ -21,13 +21,16 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl // This allows multiple screen to be listening to tags, even the same tag class NWCPaymentQueryState( - val fromServiceHex: String, - val toUserHex: String, - val replyingToHex: String, + val fromServiceHex: HexKey, + val toUserHex: HexKey, + val replyingToHex: HexKey, + val relay: NormalizedRelayUrl, ) class NWCPaymentFilterAssembler( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt index 24bed7d9bd..98e15761e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentWatcherSubAssembler.kt @@ -20,26 +20,29 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class NWCPaymentWatcherSubAssembler( client: NostrClient, allKeys: () -> Set, -) : SingleSubEoseManager(client, allKeys) { - override fun updateFilter( - keys: List, - since: Map?, - ): List? { - val fromAuthors = keys.mapTo(mutableSetOf()) { it.fromServiceHex } - val replyingToPayments = keys.mapTo(mutableSetOf()) { it.replyingToHex } - val aboutUsers = keys.mapTo(mutableSetOf()) { it.toUserHex } +) : SingleSubNoEoseCacheEoseManager(client, allKeys) { + override fun updateFilter(keys: List): List? { + if (keys.isEmpty()) return null - if (fromAuthors.isEmpty()) return null + return keys.groupBy { it.relay }.map { relayGroup -> + val fromAuthors = relayGroup.value.mapTo(mutableSetOf()) { it.fromServiceHex } + val replyingToPayments = relayGroup.value.mapTo(mutableSetOf()) { it.replyingToHex } + val aboutUsers = relayGroup.value.mapTo(mutableSetOf()) { it.toUserHex } - return filterNWCPaymentsFromRequests(fromAuthors, replyingToPayments, aboutUsers, since) + if (fromAuthors.isEmpty() || replyingToPayments.isEmpty()) return null + + RelayBasedFilter( + relay = relayGroup.key, + filter = filterNWCPaymentsFromRequests(fromAuthors, replyingToPayments, aboutUsers), + ) + } } override fun distinct(key: NWCPaymentQueryState) = key.replyingToHex diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt index 115c48be23..b7e6b8a52b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt @@ -20,15 +20,17 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.UserLoaderSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserWatcherSubAssembler -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to tags, even the same tag class UserFinderQueryState( val user: User, + val account: Account, ) class UserFinderFilterAssembler( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt index a171e007e4..7e1450331e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssemblerSubscription.kt @@ -20,28 +20,36 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user +import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@SuppressLint("StateFlowValueCalledInComposition") @Composable fun UserFinderFilterAssemblerSubscription( user: User, accountViewModel: AccountViewModel, -) = UserFinderFilterAssemblerSubscription(user, accountViewModel.dataSources().userFinder) +) = UserFinderFilterAssemblerSubscription( + user, + accountViewModel.account, + accountViewModel.dataSources().userFinder, +) @Composable fun UserFinderFilterAssemblerSubscription( user: User, + forAccount: Account, dataSource: UserFinderFilterAssembler, ) { // different screens get different states // even if they are tracking the same tag. val state = remember(user) { - UserFinderQueryState(user) + UserFinderQueryState(user, forAccount) } KeyDataSourceSubscription(state, dataSource) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index cd71650b46..9920cd4242 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -34,8 +34,8 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -455,7 +455,7 @@ fun observeUserIsFollowingChannel( remember(account) { account .publicChatList - .livePublicChatEventIdSet + .flowSet .mapLatest { followingChannels -> channel.idHex in followingChannels }.distinctUntilChanged() @@ -463,7 +463,7 @@ fun observeUserIsFollowingChannel( } @SuppressLint("StateFlowValueCalledInComposition") - return flow.collectAsStateWithLifecycle(channel.idHex in account.publicChatList.livePublicChatEventIdSet.value) + return flow.collectAsStateWithLifecycle(channel.idHex in account.publicChatList.flowSet.value) } @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @@ -603,7 +603,7 @@ fun observeUserStatuses( @Composable fun observeUserRelayIntoList( user: User, - relayUrl: String, + relayUrl: NormalizedRelayUrl, accountViewModel: AccountViewModel, ): State { // Subscribe in the relay for changes in the metadata of this user. @@ -656,8 +656,8 @@ fun observeUserRoomSubject( } data class RelayUsage( - val relays: List = emptyList(), - val userRelayList: List = emptyList(), + val relays: List = emptyList(), + val userRelayList: List = emptyList(), ) @OptIn(FlowPreview::class) @@ -677,7 +677,7 @@ fun observeUserRelaysUsing( val currentUserRelays = relayInfo.user.latestContactList ?.relays() - ?.map { RelayUrlFormatter.normalize(it.key) } ?: emptyList() + ?.map { it.key } ?: emptyList() RelayUsage(userRelaysBeingUsed, currentUserRelays) }.sample(1000) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt index 5e603897a4..f43380b2f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt @@ -20,25 +20,52 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.utils.mapOfSet +import kotlin.collections.forEach -fun filterNewUserMetadataForKey(authors: Set): List = +val MetadataAndRelayListKinds = listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - MetadataEvent.KIND, - AdvertisedRelayListEvent.KIND, - ), - authors = authors.toList(), - ), - ), + MetadataEvent.KIND, + AdvertisedRelayListEvent.KIND, ) + +fun filterFindUserMetadataForKey(author: HexKey): List { + return LocalCache.checkGetOrCreateUser(author)?.let { + filterFindUserMetadataForKey(setOf(it)) + } ?: emptyList() +} + +fun filterFindUserMetadataForKey(authors: Set): List { + val perRelayKeys = + mapOfSet { + authors.forEach { key -> + val relays = + key.authorRelayList()?.writeRelaysNorm() + ?: LocalCache.relayHints.hintsForKey(key.pubkeyHex).ifEmpty { null } + ?: key.relaysBeingUsed.keys + + relays.forEach { + add(it, key.pubkeyHex) + } + // TODO, DESPERATION: TRY ALL THE CONNECTED RELAYS. + } + } + + return perRelayKeys.map { + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = MetadataAndRelayListKinds, + authors = it.value.sorted(), + ), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt index 18f612c875..019847c5a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt @@ -20,21 +20,42 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEoseCacheEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class UserLoaderSubAssembler( client: NostrClient, allKeys: () -> Set, ) : SingleSubNoEoseCacheEoseManager(client, allKeys, invalidateAfterEose = true) { - override fun updateFilter(keys: List): List? { - val firstTimers = keys.filter { it.user.latestMetadata == null }.mapTo(mutableSetOf()) { it.user.pubkeyHex } + override fun updateFilter(keys: List): List? { + val firstTimers = mutableSetOf() + + keys.forEach { + if (it.user.latestMetadata == null) { + firstTimers.add(it.user) + } else { + null + } + } + + keys.mapTo(mutableSetOf()) { it.account }.forEach { + it.kind3FollowList.flow.value.authors.forEach { + val user = LocalCache.getOrCreateUser(it) + if (user.latestMetadata == null) { + firstTimers.add(user) + } else { + null + } + } + } if (firstTimers.isEmpty()) return null - return filterNewUserMetadataForKey(firstTimers) + return filterFindUserMetadataForKey(firstTimers) } override fun distinct(key: UserFinderQueryState) = key.user diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt index 6984ac7f94..bbb5b535f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt @@ -20,25 +20,32 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip56Reports.ReportEvent -fun filterReportsToKeys( - authors: Set, - since: Map?, -): List = - listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, +val ReportKindList = listOf(ReportEvent.KIND) + +fun filterReportsToKeysFromTrusted( + targets: Set, + trustedAccounts: Map>, + since: SincePerRelayMap?, +): List { + if (targets.isEmpty() || trustedAccounts.isEmpty()) return emptyList() + val sortedTargets = targets.sorted() + return trustedAccounts.map { + RelayBasedFilter( + relay = it.key, filter = - SincePerRelayFilter( - kinds = listOf(ReportEvent.KIND), - tags = mapOf("p" to authors.toList()), - since = since, + Filter( + kinds = ReportKindList, + authors = it.value.sorted(), + tags = mapOf("p" to sortedTargets), + since = since?.get(it.key)?.time, ), - ), - ) + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt index 69e2bc3658..049e3f9cfc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt @@ -20,36 +20,49 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +val UserMetadataForKeyKinds = + listOf( + MetadataEvent.KIND, + StatusEvent.KIND, + RelationshipStatusEvent.KIND, + AdvertisedRelayListEvent.KIND, + ChatMessageRelayListEvent.KIND, + ) + fun filterUserMetadataForKey( authors: Set, - since: Map?, -): List = - listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, + since: SincePerRelayMap?, +): List { + val relays = + authors.map { + val authorHomeRelayEventAddress = AdvertisedRelayListEvent.createAddressTag(it) + val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) + + authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null } + ?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null } + ?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay) + }.flatten().toSet() + + return relays.map { + RelayBasedFilter( + relay = it, filter = - SincePerRelayFilter( - kinds = - listOf( - MetadataEvent.KIND, - StatusEvent.KIND, - RelationshipStatusEvent.KIND, - AdvertisedRelayListEvent.KIND, - ChatMessageRelayListEvent.KIND, - ), + Filter( + kinds = UserMetadataForKeyKinds, authors = authors.toList(), - since = since, + since = since?.get(it)?.time, ), - ), - ) + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt index 5736d67af4..48efdbda3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt @@ -24,43 +24,63 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.ammolite.relays.filters.MutableTime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.mapOfSet +import kotlin.collections.flatten class UserWatcherSubAssembler( client: NostrClient, allKeys: () -> Set, ) : SingleSubEoseManager(client, allKeys) { var lastUsersOnFilter: Set = emptySet() + + /** + * This assembler saves the EOSE per user key. That EOSE includes their metadata, etc + * and reports, but only from trusted accounts (follows of all logged in users). + */ var latestEOSEs: EOSEAccountFast = EOSEAccountFast(2000) override fun newEose( - relayUrl: String, + relay: NormalizedRelayUrl, time: Long, ) { lastUsersOnFilter.forEach { - latestEOSEs.newEose(it, relayUrl, time) + latestEOSEs.newEose(it, relay, time) } - super.newEose(relayUrl, time) + super.newEose(relay, time) } override fun updateFilter( keys: List, - since: Map?, - ): List? { + since: SincePerRelayMap?, + ): List? { if (keys.isEmpty()) return null - lastUsersOnFilter = keys.filter { it.user.latestMetadata != null }.map { it.user }.toSet() + lastUsersOnFilter = + keys.mapNotNullTo(mutableSetOf()) { + if (it.user.latestMetadata != null) it.user else null + } if (lastUsersOnFilter.isEmpty()) return null + val trustedAccounts = + mapOfSet { + keys.mapTo(mutableSetOf()) { it.account }.map { it.followsPerRelay.value }.forEach { + add(it) + } + } + return groupByRelayPresence(lastUsersOnFilter, latestEOSEs) .map { group -> val groupIds = group.map { it.pubkeyHex }.toSet() if (groupIds.isNotEmpty()) { val minEOSEs = findMinimumEOSEsForUsers(group, latestEOSEs) - filterUserMetadataForKey(groupIds, minEOSEs) + filterReportsToKeys(groupIds, minEOSEs) + filterUserMetadataForKey(groupIds, minEOSEs) + + filterReportsToKeysFromTrusted(groupIds, trustedAccounts, minEOSEs) } else { emptyList() } @@ -82,16 +102,16 @@ class UserWatcherSubAssembler( fun findMinimumEOSEsForUsers( users: List, eoseCache: EOSEAccountFast, - ): Map { - val minLatestEOSEs = mutableMapOf() + ): SincePerRelayMap { + val minLatestEOSEs = mutableMapOf() users.forEach { eoseCache.since(it)?.forEach { val minEose = minLatestEOSEs[it.key] if (minEose == null) { - minLatestEOSEs.put(it.key, EOSETime(it.value.time)) - } else if (it.value.time < minEose.time) { - minEose.time = it.value.time + minLatestEOSEs.put(it.key, it.value.copy()) + } else { + minEose.updateIfOlder(it.value.time) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt index 8d9f40ea7a..caa17059f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt @@ -21,11 +21,12 @@ package com.vitorpamplona.amethyst.service.relayClient.searchCommand import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.MutableComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.MutableQueryState import com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies.SearchWatcherSubAssembler -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -33,14 +34,15 @@ import kotlinx.coroutines.flow.MutableStateFlow @Stable class SearchQueryState( val searchQuery: MutableStateFlow, + val account: Account, ) : MutableQueryState { override fun flow(): Flow = searchQuery } class SearchFilterAssembler( - val cache: LocalCache, client: NostrClient, scope: CoroutineScope, + val cache: LocalCache, ) : MutableComposeSubscriptionManager(scope) { val group = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt index 3f1155159c..db63d07bbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt @@ -20,43 +20,17 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies -import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress fun filterByAddress(address: NAddress) = - listOf( - TypedFilter( - types = ALL_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(MetadataEvent.KIND), - authors = listOfNotNull(address.author), - limit = 1, - ), + filterMissingAddressables( + setOf( + Address( + address.kind, + address.author, + address.dTag, + ), ), - if (address.kind < 25000 && address.dTag.isBlank()) { - TypedFilter( - types = ALL_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(address.kind), - authors = listOf(address.author), - limit = 5, - ), - ) - } else { - TypedFilter( - types = ALL_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(address.kind), - tags = mapOf("d" to listOf(address.dTag)), - authors = listOf(address.author), - limit = 5, - ), - ) - }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt index 8f84a487c8..4be93be019 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt @@ -20,21 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies -import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.filterFindUserMetadataForKey import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -fun filterByAuthor(pubKey: HexKey) = - listOf( - TypedFilter( - types = ALL_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(MetadataEvent.KIND), - authors = listOfNotNull(pubKey), - limit = 1, - ), - ), - ) +fun filterByAuthor(pubKey: HexKey) = filterFindUserMetadataForKey(pubKey) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt index d206c94159..950b392ae4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt @@ -20,18 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies -import com.vitorpamplona.ammolite.relays.ALL_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents import com.vitorpamplona.quartz.nip01Core.core.HexKey -fun filterByEvent(eventId: HexKey) = - listOf( - TypedFilter( - types = ALL_FEED_TYPES, - filter = - SincePerRelayFilter( - ids = listOfNotNull(eventId), - ), - ), - ) +fun filterByEvent(eventId: HexKey) = filterMissingEvents(setOf(eventId)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPeopleByName.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPeopleByName.kt index 3800ca745b..8c3a5c0956 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPeopleByName.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPeopleByName.kt @@ -20,21 +20,23 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -fun searchPeopleByName(searchString: HexKey) = - listOf( - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - SincePerRelayFilter( - kinds = listOf(MetadataEvent.KIND), - search = searchString, - limit = 1000, - ), - ), - ) +fun searchPeopleByName( + searchString: HexKey, + relay: NormalizedRelayUrl, +) = listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(MetadataEvent.KIND), + search = searchString, + limit = 1000, + ), + ), +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt index 292f15f287..8392cdf547 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt @@ -20,9 +20,6 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent @@ -30,6 +27,9 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent @@ -47,62 +47,70 @@ import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefiniti import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -fun searchPostsByText(searchString: HexKey) = +val SearchPostsByTextKinds1 = listOf( - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - LongTextNoteEvent.KIND, - BadgeDefinitionEvent.KIND, - PeopleListEvent.KIND, - BookmarkListEvent.KIND, - AudioHeaderEvent.KIND, - AudioTrackEvent.KIND, - PinListEvent.KIND, - PollNoteEvent.KIND, - ChannelCreateEvent.KIND, - ), - search = searchString, - limit = 100, - ), - ), - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - SincePerRelayFilter( - kinds = - listOf( - ChannelMetadataEvent.KIND, - ClassifiedsEvent.KIND, - CommunityDefinitionEvent.KIND, - EmojiPackEvent.KIND, - HighlightEvent.KIND, - LiveActivitiesEvent.KIND, - PollNoteEvent.KIND, - NNSEvent.KIND, - WikiNoteEvent.KIND, - CommentEvent.KIND, - ), - search = searchString, - limit = 100, - ), - ), - TypedFilter( - types = setOf(FeedType.SEARCH), - filter = - SincePerRelayFilter( - kinds = - listOf( - InteractiveStoryPrologueEvent.KIND, - InteractiveStorySceneEvent.KIND, - FollowListEvent.KIND, - ), - search = searchString, - limit = 100, - ), - ), + TextNoteEvent.KIND, + LongTextNoteEvent.KIND, + BadgeDefinitionEvent.KIND, + PeopleListEvent.KIND, + BookmarkListEvent.KIND, + AudioHeaderEvent.KIND, + AudioTrackEvent.KIND, + PinListEvent.KIND, + PollNoteEvent.KIND, + ChannelCreateEvent.KIND, ) + +val SearchPostsByTextKinds2 = + listOf( + ChannelMetadataEvent.KIND, + ClassifiedsEvent.KIND, + CommunityDefinitionEvent.KIND, + EmojiPackEvent.KIND, + HighlightEvent.KIND, + LiveActivitiesEvent.KIND, + PollNoteEvent.KIND, + NNSEvent.KIND, + WikiNoteEvent.KIND, + CommentEvent.KIND, + ) + +val SearchPostsByTextKinds3 = + listOf( + InteractiveStoryPrologueEvent.KIND, + InteractiveStorySceneEvent.KIND, + FollowListEvent.KIND, + ) + +fun searchPostsByText( + searchString: HexKey, + relay: NormalizedRelayUrl, +) = listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = SearchPostsByTextKinds1, + search = searchString, + limit = 100, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = SearchPostsByTextKinds2, + search = searchString, + limit = 100, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = SearchPostsByTextKinds3, + search = searchString, + limit = 100, + ), + ), +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt index 2823f76c78..0b9657cef2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt @@ -23,12 +23,10 @@ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassembli import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed @@ -51,8 +49,8 @@ class SearchWatcherSubAssembler( ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: SearchQueryState, - since: Map?, - ): List? { + since: SincePerRelayMap?, + ): List? { val mySearchString = key.searchQuery.value if (mySearchString.isBlank()) return null @@ -63,25 +61,34 @@ class SearchWatcherSubAssembler( val key = Hex.decode(mySearchString).toHexKey() filterByAuthor(key) + filterByEvent(key) } else { - when (val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity) { - is NSec -> filterByAuthor(Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey()) - is NPub -> filterByAuthor(parsed.hex) - is NProfile -> filterByAuthor(parsed.hex) - is Note -> filterByEvent(parsed.hex) - is NEvent -> filterByEvent(parsed.hex) - is NEmbed -> { - cache.justConsume(parsed.event, null, false) - emptyList() - } + val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity + if (parsed != null) { + cache.consume(parsed) - is NRelay -> emptyList() - is NAddress -> filterByAddress(parsed) - else -> emptyList() + when (parsed) { + is NSec -> filterByAuthor(parsed.toPubKeyHex()) + is NPub -> filterByAuthor(parsed.hex) + is NProfile -> filterByAuthor(parsed.hex) + is Note -> filterByEvent(parsed.hex) + is NEvent -> filterByEvent(parsed.hex) + is NEmbed -> emptyList() + is NRelay -> emptyList() + is NAddress -> filterByAddress(parsed) + else -> emptyList() + } + } else { + emptyList() } } }.getOrDefault(emptyList()) - val searchFilters = searchPeopleByName(mySearchString) + searchPostsByText(mySearchString) + val searchFilters = + key.account.searchRelayList.flow.value.flatMap { + searchPeopleByName(mySearchString, it) + } + + key.account.searchRelayList.flow.value.flatMap { + searchPostsByText(mySearchString, it) + } return directFilters + searchFilters } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index b230de2dac..037c427e3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -22,22 +22,24 @@ package com.vitorpamplona.amethyst.service.relays import androidx.collection.LruCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.ammolite.relays.filters.MutableTime +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlin.collections.plus + +typealias SincePerRelayMap = Map class EOSERelayList { - var relayList: Map = emptyMap() + var relayList: SincePerRelayMap = emptyMap() fun addOrUpdate( - relayUrl: String, + relayUrl: NormalizedRelayUrl, time: Long, ) { val eose = relayList[relayUrl] if (eose == null) { - relayList = relayList + Pair(relayUrl, EOSETime(time)) + relayList = relayList + Pair(relayUrl, MutableTime(time)) } else { - if (time > eose.time) { - eose.update(time) - } + eose.updateIfNewer(time) } } @@ -48,9 +50,9 @@ class EOSERelayList { fun since() = relayList fun newEose( - relayUrl: String, + relay: NormalizedRelayUrl, time: Long, - ) = addOrUpdate(relayUrl, time) + ) = addOrUpdate(relay, time) } class EOSEFollowList( @@ -60,7 +62,7 @@ class EOSEFollowList( fun addOrUpdate( listCode: String, - relayUrl: String, + relayUrl: NormalizedRelayUrl, time: Long, ) { val relayList = followList[listCode] @@ -77,7 +79,7 @@ class EOSEFollowList( fun newEose( listCode: String, - relayUrl: String, + relayUrl: NormalizedRelayUrl, time: Long, ) = addOrUpdate(listCode, relayUrl, time) } @@ -90,7 +92,7 @@ class EOSEAccount( fun addOrUpdate( user: User, listCode: String, - relayUrl: String, + relayUrl: NormalizedRelayUrl, time: Long, ) { val followList = users[user] @@ -115,7 +117,7 @@ class EOSEAccount( fun newEose( user: User, listCode: String, - relayUrl: String, + relayUrl: NormalizedRelayUrl, time: Long, ) = addOrUpdate(user, listCode, relayUrl, time) } @@ -127,7 +129,7 @@ class EOSEAccountFast( fun addOrUpdate( user: T, - relayUrl: String, + relayUrl: NormalizedRelayUrl, time: Long, ) { val relayList = users[user] @@ -149,7 +151,7 @@ class EOSEAccountFast( fun newEose( user: T, - relayUrl: String, + relayUrl: NormalizedRelayUrl, time: Long, ) = addOrUpdate(user, relayUrl, time) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 6b294ed187..6e9ab4cec9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -32,11 +32,11 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.uploads.MediaUploadResult -import com.vitorpamplona.amethyst.service.uploads.nip96.randomChars import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent +import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.sha256.sha256 import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -129,7 +129,7 @@ class BlossomUploader { ): MediaUploadResult { checkNotInMainThread() - val fileName = baseFileName ?: randomChars() + val fileName = baseFileName ?: RandomInstance.randomChars(16) val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt index 706544a0db..a6dca1bda7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.nip96FileStorage.actions.PartialEvent import com.vitorpamplona.quartz.nip96FileStorage.actions.UploadResult import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfo import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent +import com.vitorpamplona.quartz.utils.RandomInstance import kotlinx.coroutines.delay import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody @@ -50,10 +51,6 @@ import okio.BufferedSink import okio.source import java.io.InputStream -val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') - -fun randomChars() = List(16) { charPool.random() }.joinToString("") - class Nip96Uploader { suspend fun upload( uri: Uri, @@ -138,7 +135,7 @@ class Nip96Uploader { ): MediaUploadResult { checkNotInMainThread() - val fileName = randomChars() + val fileName = RandomInstance.randomChars(16) val extension = contentType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } ?: "" val client = okHttpClient(server.apiUrl) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 916edcf225..3b5f46db8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -34,6 +34,7 @@ import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.debugState +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING @@ -145,7 +146,10 @@ class MainActivity : AppCompatActivity() { } } -fun uriToRoute(uri: String?): Route? = +fun uriToRoute( + uri: String?, + account: Account, +): Route? = if (uri?.startsWith("notifications", true) == true || uri?.startsWith("nostr:notifications", true) == true) { Route.Notification } else { @@ -153,6 +157,9 @@ fun uriToRoute(uri: String?): Route? = Route.Hashtag(uri.removePrefix("nostr:").removePrefix("hashtag?id=")) } else { val nip19 = Nip19Parser.uriToRoute(uri)?.entity + if (nip19 != null) { + LocalCache.consume(nip19) + } when (nip19) { is NPub -> Route.Profile(nip19.hex) is NProfile -> Route.Profile(nip19.hex) @@ -181,12 +188,7 @@ fun uriToRoute(uri: String?): Route? = } } - is NEmbed -> { - if (LocalCache.getNoteIfExists(nip19.event.id) == null) { - LocalCache.justConsume(nip19.event, null, false) - } - Route.EventRedirect(nip19.event.id) - } + is NEmbed -> Route.EventRedirect(nip19.event.id) else -> null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 33dbb37759..65d19f6d86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -91,7 +91,9 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.PostButton +import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest +import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.painterRes @@ -127,7 +129,7 @@ fun EditPostView( val scrollState = rememberScrollState() val scope = rememberCoroutineScope() var showRelaysDialog by remember { mutableStateOf(false) } - var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } + var relayList = remember { accountViewModel.account.outboxRelays.flow.value.toImmutableList() } LaunchedEffect(Unit) { postViewModel.load(edit, versionLookingAt, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index f0000064b5..2ede4d54b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -45,9 +45,9 @@ import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestio import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.ammolite.relays.RelaySetupInfo import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder import com.vitorpamplona.quartz.nip94FileMetadata.alt @@ -120,18 +120,18 @@ open class EditPostViewModel : ViewModel() { editedFromNote = edit } - fun sendPost(relayList: List) { + fun sendPost(relayList: List) { viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) } } - suspend fun innerSendPost(relayList: List) { + suspend fun innerSendPost(relayList: List) { if (accountViewModel == null) { cancel() return } nip95attachments.forEach { - account?.sendNip95(it.first, it.second, relayList) + account?.sendNip95(it.first, it.second, relayList.toSet()) } val notify = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index f00f3bf993..320793b386 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.ammolite.relays.RelaySetupInfo +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.joinAll @@ -82,7 +82,7 @@ open class NewMediaModel : ViewModel() { fun upload( context: Context, - relayList: List, + relayList: List, onSucess: () -> Unit, onError: (String, String) -> Unit, ) { @@ -162,7 +162,7 @@ open class NewMediaModel : ViewModel() { caption, if (sensitiveContent) "" else null, it.uploadedHash, - relayList, + relayList.toSet(), ) { continuation.resume(true) } @@ -181,7 +181,7 @@ open class NewMediaModel : ViewModel() { imageUrls, caption, if (sensitiveContent) "" else null, - relayList, + relayList.toSet(), ) { continuation.resume(true) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 1af3b03c25..b0e2824c3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -101,7 +101,7 @@ fun NewMediaView( } var showRelaysDialog by remember { mutableStateOf(false) } - var relayList = remember { accountViewModel.account.activeWriteRelays().toImmutableList() } + var relayList = remember { accountViewModel.account.outboxRelays.flow.value.toImmutableList() } Dialog( onDismissRequest = { onClose() }, @@ -202,7 +202,7 @@ fun ImageVideoPost( accountViewModel: AccountViewModel, ) { val nip95description = stringRes(id = R.string.upload_server_relays_nip95) - val fileServers by accountViewModel.account.liveServerList.collectAsState() + val fileServers by accountViewModel.account.serverLists.liveServerList.collectAsState() val fileServerOptions = remember(fileServers) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt index a07effd54e..4b27cf41f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt @@ -58,44 +58,29 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDial import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.ammolite.relays.RelaySetupInfo +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlin.collections.map data class RelayList( - val relay: RelaySetupInfo, - val relayInfo: RelayBriefInfoCache.RelayBriefInfo, + val relay: NormalizedRelayUrl, val isSelected: Boolean, ) data class RelayInfoDialog( - val relayBriefInfo: RelayBriefInfoCache.RelayBriefInfo, + val relay: NormalizedRelayUrl, val relayInfo: Nip11RelayInformation, ) -@Composable -fun RelaySelectionDialog( - preSelectedList: ImmutableList, - onClose: () -> Unit, - onPost: (list: ImmutableList) -> Unit, - accountViewModel: AccountViewModel, - nav: INav, -) = RelaySelectionDialogEasy( - preSelectedList.map { it.url }.toImmutableList(), - onClose, - onPost, - accountViewModel, - nav, -) - @OptIn(ExperimentalMaterial3Api::class) @Composable -fun RelaySelectionDialogEasy( - preSelectedList: ImmutableList, +fun RelaySelectionDialog( + preSelectedList: ImmutableList, onClose: () -> Unit, - onPost: (list: ImmutableList) -> Unit, + onPost: (list: ImmutableList) -> Unit, accountViewModel: AccountViewModel, nav: INav, ) { @@ -103,11 +88,10 @@ fun RelaySelectionDialogEasy( var relays by remember { mutableStateOf( - accountViewModel.account.connectToRelays.value.map { + accountViewModel.account.client.connectedRelayList().map { RelayList( relay = it, - relayInfo = RelayBriefInfoCache.RelayBriefInfo(it.url), - isSelected = preSelectedList.any { relayUrl -> it.url == relayUrl }, + isSelected = preSelectedList.any { relayUrl -> it == relayUrl }, ) }, ) @@ -121,7 +105,7 @@ fun RelaySelectionDialogEasy( RelayInformationDialog( onClose = { relayInfo = null }, relayInfo = it.relayInfo, - relayBriefInfo = it.relayBriefInfo, + relay = it.relay, accountViewModel = accountViewModel, nav = nav, ) @@ -147,8 +131,8 @@ fun RelaySelectionDialogEasy( actions = { SaveButton( onPost = { - val selectedRelays = relays.filter { it.isSelected } - onPost(selectedRelays.map { it.relay }.toImmutableList()) + val selectedRelays = relays.filter { it.isSelected }.map { it.relay }.toImmutableList() + onPost(selectedRelays) onClose() }, isActive = hasSelectedRelay, @@ -185,10 +169,10 @@ fun RelaySelectionDialogEasy( ) { itemsIndexed( relays, - key = { _, item -> item.relay.url }, + key = { _, item -> item.relay }, ) { index, item -> RelaySwitch( - text = item.relayInfo.displayUrl, + text = item.relay.displayUrl(), checked = item.isSelected, onClick = { relays = @@ -202,45 +186,43 @@ fun RelaySelectionDialogEasy( }, onLongPress = { accountViewModel.retrieveRelayDocument( - item.relay.url, + relay = item.relay, onInfo = { relayInfo = RelayInfoDialog( - RelayBriefInfoCache.RelayBriefInfo( - item.relay.url, - ), + item.relay, it, ) }, - onError = { url, errorCode, exceptionMessage -> + onError = { relay, errorCode, exceptionMessage -> val msg = when (errorCode) { Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> stringRes( context, R.string.relay_information_document_error_assemble_url, - url, + relay.url, exceptionMessage, ) Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> stringRes( context, R.string.relay_information_document_error_assemble_url, - url, + relay.url, exceptionMessage, ) Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> stringRes( context, R.string.relay_information_document_error_assemble_url, - url, + relay.url, exceptionMessage, ) Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> stringRes( context, R.string.relay_information_document_error_assemble_url, - url, + relay.url, exceptionMessage, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt index 2a249fd45d..8172f7fea8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt @@ -127,5 +127,5 @@ class BlossomServersViewModel : ViewModel() { } } - private fun obtainFileServers(): List? = account.getBlossomServersList()?.servers() + private fun obtainFileServers(): List? = account.blossomServers.fileServers.value } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/NIP96ServersViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/NIP96ServersViewModel.kt index 98cfae496f..bd50c97e97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/NIP96ServersViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/NIP96ServersViewModel.kt @@ -122,5 +122,5 @@ class NIP96ServersViewModel : ViewModel() { } } - private fun obtainFileServers(): List? = account.getFileServersList()?.servers() + private fun obtainFileServers(): List? = account.fileStorageServers.fileServers.value } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 41d68ea817..7a9a8f6a31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -164,6 +164,7 @@ fun RenderStrangeNamePreview() { @Composable fun RenderRegularPreview() { val nav = EmptyNav + val accountViewModel = mockAccountViewModel() Column(modifier = Modifier.padding(10.dp)) { RenderRegular( @@ -190,7 +191,7 @@ fun RenderRegularPreview() { ) } - is HashTagSegment -> HashTag(word, nav) + is HashTagSegment -> HashTag(word, accountViewModel, nav) // is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) // is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) @@ -204,6 +205,7 @@ fun RenderRegularPreview() { @Composable fun RenderRegularPreview2() { val nav = EmptyNav + val accountViewModel = mockAccountViewModel() RenderRegular( "#Amethyst v0.84.1: ncryptsec support (NIP-49)", EmptyTagList, @@ -218,7 +220,7 @@ fun RenderRegularPreview2() { is EmailSegment -> ClickableEmail(word.segmentText) is PhoneSegment -> ClickablePhone(word.segmentText) // is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav) - is HashTagSegment -> HashTag(word, nav) + is HashTagSegment -> HashTag(word, accountViewModel, nav) // is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) // is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) @@ -259,7 +261,7 @@ fun RenderRegularPreview3() { is EmailSegment -> ClickableEmail(word.segmentText) is PhoneSegment -> ClickablePhone(word.segmentText) // is BechSegment -> BechLink(word.segmentText, true, backgroundColor, accountViewModel, nav) - is HashTagSegment -> HashTag(word, nav) + is HashTagSegment -> HashTag(word, accountViewModel, nav) // is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) // is HashIndexEventSegment -> TagLink(word, true, backgroundColorState, accountViewModel, nav) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) @@ -386,7 +388,7 @@ private fun RenderWordWithoutPreview( is SecretEmoji -> Text(word.segmentText) is PhoneSegment -> ClickablePhone(word.segmentText) is BechSegment -> BechLink(word.segmentText, false, 0, backgroundColor, accountViewModel, nav) - is HashTagSegment -> HashTag(word, nav) + is HashTagSegment -> HashTag(word, accountViewModel, nav) is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) is HashIndexEventSegment -> TagLink(word, false, 0, backgroundColor, accountViewModel, nav) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) @@ -415,7 +417,7 @@ private fun RenderWordWithPreview( is SecretEmoji -> DisplaySecretEmoji(word, state, callbackUri, true, quotesLeft, backgroundColor, accountViewModel, nav) is PhoneSegment -> ClickablePhone(word.segmentText) is BechSegment -> BechLink(word.segmentText, true, quotesLeft, backgroundColor, accountViewModel, nav) - is HashTagSegment -> HashTag(word, nav) + is HashTagSegment -> HashTag(word, accountViewModel, nav) is HashIndexUserSegment -> TagLink(word, accountViewModel, nav) is HashIndexEventSegment -> TagLink(word, true, quotesLeft, backgroundColor, accountViewModel, nav) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word) @@ -620,6 +622,7 @@ fun CoreSecretMessage( @Composable fun HashTag( segment: HashTagSegment, + accountViewModel: AccountViewModel, nav: INav, ) { val primary = MaterialTheme.colorScheme.primary diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt index cc4f5356b8..59ef87311e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt @@ -73,7 +73,7 @@ fun RenderContentAsMarkdown( val onClick = remember { { link: String -> - val route = uriToRoute(link) + val route = uriToRoute(link, accountViewModel.account) if (route != null) { nav.nav(route) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt index 3243d283ee..0448f0275b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt @@ -20,26 +20,22 @@ */ package com.vitorpamplona.amethyst.ui.dal -import com.vitorpamplona.amethyst.model.AROUND_ME -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS +import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNotes -import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHashes -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHashes -import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.TimeUtils class FilterByListParams( - val isGlobal: Boolean, val isHiddenList: Boolean, - val isAroundMe: Boolean, - val followLists: Account.LiveFollowList?, - val hiddenLists: Account.LiveHiddenUsers, + val followLists: IFeedTopNavFilter?, + val hiddenLists: HiddenUsersState.LiveHiddenUsers, val now: Long = TimeUtils.oneMinuteFromNow(), ) { fun isNotHidden(userHex: String) = !(hiddenLists.hiddenUsers.contains(userHex) || hiddenLists.spammers.contains(userHex)) @@ -48,45 +44,39 @@ class FilterByListParams( fun isEventInList(noteEvent: Event): Boolean { if (followLists == null) return false - if (isAroundMe && followLists.geotags.isEmpty()) return false - return if (noteEvent is LiveActivitiesEvent) { - noteEvent.participantsIntersect(followLists.authors) || - noteEvent.isTaggedHashes(followLists.hashtags) || - noteEvent.isTaggedGeoHashes(followLists.geotags) || - noteEvent.isTaggedAddressableNotes(followLists.addresses) - } else if (noteEvent is CommentEvent) { - // ignore follows and checks only the root scope - noteEvent.isTaggedHashes(followLists.hashtags) || - noteEvent.isTaggedScopes(followLists.hashtagScopes) || - noteEvent.isTaggedGeoHashes(followLists.geotags) || - noteEvent.isTaggedScopes(followLists.geotagScopes) || - noteEvent.isTaggedAddressableNotes(followLists.addresses) - } else { - noteEvent.pubKey in followLists.authors || - noteEvent.isTaggedHashes(followLists.hashtags) || - noteEvent.isTaggedGeoHashes(followLists.geotags) || - noteEvent.isTaggedAddressableNotes(followLists.addresses) - } + return followLists.match(noteEvent) + } + + fun isAuthorInFollows(author: HexKey): Boolean { + if (followLists == null) return false + + return followLists.matchAuthor(author) } fun isAuthorInFollows(address: Address): Boolean { if (followLists == null) return false - return address.pubKeyHex in followLists.authors + return followLists.matchAuthor(address.pubKeyHex) } + fun isGlobal(comingFrom: List) = + followLists is GlobalTopNavFilter && + comingFrom.any { followLists.relays.value.contains(it) } + fun match( noteEvent: Event, - isGlobalRelay: Boolean = true, - ) = ((isGlobal && isGlobalRelay) || isEventInList(noteEvent)) && + comingFrom: List = emptyList(), + ) = ((isGlobal(comingFrom)) || isEventInList(noteEvent)) && (isHiddenList || isNotHidden(noteEvent.pubKey)) && isNotInTheFuture(noteEvent) - fun match(address: Address?) = - address != null && - (isGlobal || isAuthorInFollows(address)) && - (isHiddenList || isNotHidden(address.pubKeyHex)) + fun match( + address: Address?, + comingFrom: List = emptyList(), + ) = address != null && + (isGlobal(comingFrom) || isAuthorInFollows(address)) && + (isHiddenList || isNotHidden(address.pubKeyHex)) companion object { fun showHiddenKey( @@ -95,15 +85,11 @@ class FilterByListParams( ) = selectedListName == PeopleListEvent.blockListFor(userHex) || selectedListName == MuteListEvent.blockListFor(userHex) fun create( - userHex: String, - selectedListName: String, - followLists: Account.LiveFollowList?, - hiddenUsers: Account.LiveHiddenUsers, + followLists: IFeedTopNavFilter?, + hiddenUsers: HiddenUsersState.LiveHiddenUsers, ): FilterByListParams = FilterByListParams( - isGlobal = selectedListName == GLOBAL_FOLLOWS, - isHiddenList = showHiddenKey(selectedListName, userHex), - isAroundMe = selectedListName == AROUND_ME, + isHiddenList = followLists is MutedAuthorsByOutboxTopNavFilter, followLists = followLists, hiddenLists = hiddenUsers, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index aee8a9f70c..37cf325504 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -89,6 +89,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -129,13 +130,17 @@ fun AppNavigation( composableFromEndArgs { DvmContentDiscoveryScreen(it.id, accountViewModel, nav) } composableFromEndArgs { ProfileScreen(it.id, accountViewModel, nav) } composableFromEndArgs { ThreadScreen(it.id, accountViewModel, nav) } - composableFromEndArgs { HashtagScreen(it.id, accountViewModel, nav) } - composableFromEndArgs { GeoHashScreen(it.id, accountViewModel, nav) } + composableFromEndArgs { HashtagScreen(it, accountViewModel, nav) } + composableFromEndArgs { GeoHashScreen(it, accountViewModel, nav) } composableFromEndArgs { CommunityScreen(it.id, accountViewModel, nav) } composableFromEndArgs { ChatroomScreen(it.id.toString(), it.message, it.replyId, it.draftId, accountViewModel, nav) } composableFromEndArgs { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) } composableFromEndArgs { ChannelScreen(it.id, accountViewModel, nav) } - composableFromEndArgs { EphemeralChatScreen(RoomId(it.id, it.relayUrl), accountViewModel, nav) } + composableFromEndArgs { + RelayUrlNormalizer.normalizeOrNull(it.relayUrl)?.let { relay -> + EphemeralChatScreen(RoomId(it.id, relay), accountViewModel, nav) + } + } composableFromBottomArgs { ChannelMetadataScreen(it.id, accountViewModel, nav) } composableFromBottomArgs { NewEphemeralChatScreen(accountViewModel, nav) } @@ -268,7 +273,7 @@ private fun NavigateIfIntentRequested( currentIntentNextPage?.let { intentNextPage -> var actionableNextPage by remember { - mutableStateOf(uriToRoute(intentNextPage)) + mutableStateOf(uriToRoute(intentNextPage, accountViewModel.account)) } LaunchedEffect(intentNextPage) { @@ -326,7 +331,7 @@ private fun NavigateIfIntentRequested( if (!uri.isNullOrBlank()) { // navigation functions - val newPage = uriToRoute(uri) + val newPage = uriToRoute(uri, accountViewModel.account) if (newPage != null) { scope.launch { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/BottomBarRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/BottomBarRoute.kt new file mode 100644 index 0000000000..dacf6b471d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/BottomBarRoute.kt @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.navigation + +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.ui.theme.Size20dp +import com.vitorpamplona.amethyst.ui.theme.Size23dp + +class BottomBarRoute( + val route: Route, + val icon: Int, + val contentDescriptor: Int, + val notifSize: Modifier = Modifier.size(Size23dp), + val iconSize: Modifier = Modifier.size(Size20dp), +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 687b012bbb..d7eb4f753f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -120,8 +120,8 @@ import com.vitorpamplona.amethyst.ui.theme.bannerModifier import com.vitorpamplona.amethyst.ui.theme.drawerSpacing import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.profileContentHeaderModifier -import com.vitorpamplona.ammolite.relays.RelayPoolStatus import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists @@ -393,7 +393,7 @@ private fun FollowingAndFollowerCounts( Row( modifier = drawerSpacing.clickable(onClick = onClick), ) { - val followingCount = baseAccountUser.liveKind3Follows.collectAsStateWithLifecycle() + val followingCount = baseAccountUser.kind3FollowList.flow.collectAsStateWithLifecycle() val followerCount by observeUserFollowerCount(baseAccountUser.userProfile(), accountViewModel) Text( @@ -528,15 +528,15 @@ fun ListContent( @Composable private fun RelayStatus(accountViewModel: AccountViewModel) { - val connectedRelaysText by accountViewModel.relayStatusFlow().collectAsStateWithLifecycle(RelayPoolStatus(0, 0)) + val connectedRelaysText by accountViewModel.relayStatusFlow().collectAsStateWithLifecycle() RenderRelayStatus(connectedRelaysText) } @Composable -private fun RenderRelayStatus(relayPool: RelayPoolStatus) { +private fun RenderRelayStatus(relayPool: RelayPool.RelayPoolStatus) { val text by - remember(relayPool) { derivedStateOf { "${relayPool.connected}/${relayPool.available}" } } + remember(relayPool) { derivedStateOf { "${relayPool.connected.size}/${relayPool.available.size}" } } val placeHolder = MaterialTheme.colorScheme.placeholderText diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/MutableSubscriptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt similarity index 57% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/MutableSubscriptionCache.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt index 9d78017a3f..1db3eaffe4 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/MutableSubscriptionCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt @@ -18,27 +18,26 @@ * 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.ammolite.relays +package com.vitorpamplona.amethyst.ui.navigation -class MutableSubscriptionCache : SubscriptionCache { - private var subscriptions = mapOf>() +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder - fun add( - subscriptionId: String, - filters: List = listOf(), +object NormalizedRelayUrlSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("NormalizedRelayUrl", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): NormalizedRelayUrl { + return NormalizedRelayUrl(decoder.decodeString()) + } + + override fun serialize( + encoder: Encoder, + value: NormalizedRelayUrl, ) { - subscriptions = subscriptions + Pair(subscriptionId, filters) + encoder.encodeString(value.url) } - - fun remove(subscriptionId: String) { - subscriptions = subscriptions.minus(subscriptionId) - } - - override fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId) - - override fun allSubscriptions(): Map> = subscriptions - - override fun getSubscriptionFilters(subId: String): List = subscriptions[subId] ?: emptyList() - - override fun getSubscriptionFiltersOrNull(subId: String): List? = subscriptions[subId] } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt index cc3515cb39..13e608b3c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt @@ -171,12 +171,12 @@ fun routeToMessage( fun routeFor(note: Channel): Route = if (note is EphemeralChatChannel) { - Route.EphemeralChat(note.roomId.id, note.roomId.relayUrl) + Route.EphemeralChat(note.roomId.id, note.roomId.relayUrl.url) } else { Route.Channel(note.idHex) } -fun routeFor(roomId: RoomId): Route = Route.EphemeralChat(roomId.id, roomId.relayUrl) +fun routeFor(roomId: RoomId): Route = Route.EphemeralChat(roomId.id, roomId.relayUrl.url) fun routeFor(user: User): Route.Profile = Route.Profile(user.pubkeyHex) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index eced5ad9ca..06d222e803 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -20,25 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.navigation -import androidx.compose.foundation.layout.size -import androidx.compose.ui.Modifier import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavHostController import androidx.navigation.toRoute -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.theme.Size20dp -import com.vitorpamplona.amethyst.ui.theme.Size23dp import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.serialization.Serializable -class BottomBarRoute( - val route: Route, - val icon: Int, - val contentDescriptor: Int = R.string.route, - val notifSize: Modifier = Modifier.size(Size23dp), - val iconSize: Modifier = Modifier.size(Size20dp), -) - sealed class Route { @Serializable object Home : Route() @@ -85,11 +72,11 @@ sealed class Route { ) : Route() @Serializable data class Hashtag( - val id: String, + val hashtag: String, ) : Route() @Serializable data class Geohash( - val id: String, + val geohash: String, ) : Route() @Serializable data class Community( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 874d2597ec..a1fba8d9dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -212,7 +212,7 @@ fun LoadChannel( accountViewModel: AccountViewModel, content: @Composable (EphemeralChatChannel) -> Unit, ) { - var channel = + val channel = produceStateIfNotNull(accountViewModel.getChannelIfExists(id) as? EphemeralChatChannel, id) { value = accountViewModel.checkGetOrCreateChannel(id) as? EphemeralChatChannel } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 9595dc5763..c55ea2a92d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -977,7 +977,7 @@ fun SecondUserInfoRow( val geo = remember(noteEvent) { noteEvent.geoHashOrScope() } if (geo != null) { Spacer(StdHorzSpacer) - DisplayLocation(geo, nav) + DisplayLocation(geo, accountViewModel, nav) } val baseReward = remember(noteEvent) { noteEvent.bountyBaseReward()?.let { Reward(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index aa8ca82855..244bd30c92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -107,6 +107,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReposts import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteRepostsBy import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.ui.components.ClickableBox @@ -1156,6 +1157,12 @@ fun ObserveZapIcon( if (!wasZappedByLoggedInUser.value) { val zapsState by observeNoteZaps(baseNote, accountViewModel) + zapsState?.note?.zapPayments?.forEach { + if (it.value == null) { + NWCFinderFilterAssemblerSubscription(it.key, accountViewModel) + } + } + LaunchedEffect(key1 = zapsState) { if (zapsState?.note?.zapPayments?.isNotEmpty() == true || zapsState?.note?.zaps?.isNotEmpty() == true) { accountViewModel.calculateIfNoteWasZappedByAccount(baseNote) { newWasZapped -> @@ -1179,6 +1186,12 @@ fun ObserveZapAmountText( val zapsState by observeNoteZaps(baseNote, accountViewModel) if (zapsState?.note?.zapPayments?.isNotEmpty() == true) { + zapsState?.note?.zapPayments?.forEach { + if (it.value == null) { + NWCFinderFilterAssemblerSubscription(it.key, accountViewModel) + } + } + @Suppress("ProduceStateDoesNotAssignValue") val zapAmountTxt by produceState(initialValue = showAmount(baseNote.zapsAmount), key1 = zapsState) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt index 7aa662cf16..4fa5b6d89b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -70,7 +71,7 @@ fun RelayCompose( ) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { Text( - relay.url.trim().removePrefix("wss://"), + text = relay.url.displayUrl(), fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 617c9bc0c1..5317a458eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -66,7 +66,8 @@ import com.vitorpamplona.amethyst.ui.theme.StdStartPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.relayIconModifier import com.vitorpamplona.amethyst.ui.theme.ripple24dp -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @Composable public fun RelayBadgesHorizontal( @@ -117,11 +118,11 @@ fun ChatRelayExpandButton(onClick: () -> Unit) { @OptIn(ExperimentalFoundationApi::class) @Composable fun RenderRelay( - relay: RelayBriefInfoCache.RelayBriefInfo, + relay: NormalizedRelayUrl, accountViewModel: AccountViewModel, nav: INav, ) { - val relayInfo by loadRelayInfo(relay.url, accountViewModel) + val relayInfo by loadRelayInfo(relay, accountViewModel) var openRelayDialog by remember { mutableStateOf(false) } @@ -130,7 +131,7 @@ fun RenderRelay( RelayInformationDialog( onClose = { openRelayDialog = false }, relayInfo = it, - relayBriefInfo = relay, + relay = relay, accountViewModel = accountViewModel, nav = nav, ) @@ -150,7 +151,7 @@ fun RenderRelay( }, onClick = { accountViewModel.retrieveRelayDocument( - relay.url, + relay, onInfo = { openRelayDialog = true }, @@ -170,7 +171,7 @@ fun RenderRelay( Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> R.string.relay_information_document_error_failed_with_http }, - url, + url.url, exceptionMessage ?: errorCode.toString(), ) }, @@ -184,8 +185,8 @@ fun RenderRelay( contentAlignment = Alignment.Center, ) { RenderRelayIcon( - displayUrl = relay.displayUrl, - iconUrl = relayInfo?.icon ?: relay.favIcon, + displayUrl = relay.displayUrl(), + iconUrl = relayInfo?.icon, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, pingInMs = 0, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 880c5ecd76..2db170fbe5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -100,11 +100,11 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.decodePrivateKeyAsHexOrNull import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -139,7 +139,7 @@ class UpdateZapAmountViewModel : ViewModel() { ?.let { TextFieldValue(it) } ?: TextFieldValue("") this.walletConnectRelay = myAccount.settings.zapPaymentRequest - ?.relayUri + ?.relayUri?.url ?.let { TextFieldValue(it) } ?: TextFieldValue("") this.walletConnectSecret = myAccount.settings.zapPaymentRequest @@ -173,11 +173,11 @@ class UpdateZapAmountViewModel : ViewModel() { null } - val relayUrl = walletConnectRelay.text.ifBlank { null }?.let { RelayUrlFormatter.normalize(it) } + val relayUrl = walletConnectRelay.text.ifBlank { null }?.let { RelayUrlNormalizer.normalizeOrNull(it) } val privKeyHex = walletConnectSecret.text.ifBlank { null }?.let { decodePrivateKeyAsHexOrNull(it) } if (pubkeyHex != null && relayUrl != null) { - Nip47WalletConnect.Nip47URI( + Nip47WalletConnect.Nip47URINorm( pubkeyHex, relayUrl, privKeyHex, @@ -224,7 +224,7 @@ class UpdateZapAmountViewModel : ViewModel() { fun updateNIP47(uri: String) { val contact = Nip47WalletConnect.parse(uri) walletConnectPubkey = TextFieldValue(contact.pubKeyHex) - walletConnectRelay = TextFieldValue(contact.relayUri) + walletConnectRelay = TextFieldValue(contact.relayUri.url) walletConnectSecret = TextFieldValue(contact.secret ?: "") } } @@ -429,8 +429,7 @@ fun UpdateZapAmountContent( ) { TextSpinner( label = stringRes(id = R.string.zap_type_explainer), - placeholder = - zapTypes.filter { it.first == accountViewModel.defaultZapType() }.first().second, + placeholder = zapTypes.firstOrNull { it.first == accountViewModel.defaultZapType() }?.second ?: zapTypes.firstOrNull()?.second ?: "", options = zapOptions, onSelect = { postViewModel.selectedZapType = zapTypes[it].first }, modifier = Modifier.weight(1f).padding(end = 5.dp), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index ee92e08b65..c2c9eeb778 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -373,7 +373,7 @@ fun WatchUserFollows( if (accountViewModel.isLoggedUser(userHex)) { onFollowChanges(true) } else { - val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle() + val state by accountViewModel.account.kind3FollowList.flow.collectAsStateWithLifecycle() onFollowChanges(state.authors.contains(userHex)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt index 9d5e150b97..baf2356eaa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt @@ -98,7 +98,7 @@ fun ImageVideoDescription( ) { val nip95description = stringRes(id = R.string.upload_server_relays_nip95) - val fileServers by accountViewModel.account.liveServerList.collectAsState() + val fileServers by accountViewModel.account.serverLists.liveServerList.collectAsState() val fileServerOptions = remember(fileServers, includeNIP95) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt index 7bbf6fb9d6..453ab6a778 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/UserSuggestionState.kt @@ -42,7 +42,7 @@ class UserSuggestionState( ) { val invalidations = MutableStateFlow(0) val currentWord = MutableStateFlow("") - val searchDataSourceState = SearchQueryState(MutableStateFlow("")) + val searchDataSourceState = SearchQueryState(MutableStateFlow(""), accountViewModel.account) @OptIn(FlowPreview::class) val searchTerm = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index 074168dc71..752ac98dfa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -50,7 +50,7 @@ fun DisplayFollowingHashtagsInPost( accountViewModel: AccountViewModel, nav: INav, ) { - val userFollowState by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle() + val userFollowState by accountViewModel.account.allFollows.flow.collectAsStateWithLifecycle() var firstTag by remember(baseNote) { mutableStateOf(null) } LaunchedEffect(key1 = userFollowState) { @@ -70,7 +70,7 @@ fun DisplayFollowingHashtagsInPost( firstTag?.let { Column(verticalArrangement = Arrangement.Center) { - Row(verticalAlignment = Alignment.CenterVertically) { DisplayTagList(it, nav) } + Row(verticalAlignment = Alignment.CenterVertically) { DisplayTagList(it, accountViewModel, nav) } } } } @@ -78,6 +78,7 @@ fun DisplayFollowingHashtagsInPost( @Composable private fun DisplayTagList( firstTag: String, + accountViewModel: AccountViewModel, nav: INav, ) { Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt index 962c771be3..2a6dd3af22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt @@ -31,11 +31,13 @@ import androidx.compose.ui.text.withLink import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Font14SP @Composable fun DisplayLocation( geohashStr: String, + accountViewModel: AccountViewModel, nav: INav, ) { LoadCityName(geohashStr) { cityName -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index 6dd9e708d3..40d9e0c499 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -70,7 +70,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.math.BigDecimal @@ -181,7 +180,6 @@ class AddBountyAmountViewModel : ViewModel() { newValue, bountyInner, draftTag = null, - relayList = myAccount.activeWriteRelays().toImmutableList(), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt index 5d06653453..e27bdaed5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.ui.components.ClickableTextColor import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.ui.theme.lessImportantLink import com.vitorpamplona.quartz.nip01Core.core.Event @@ -43,9 +44,10 @@ import kotlinx.coroutines.launch fun DisplayUncitedHashtags( event: Event, callbackUri: String? = null, + accountViewModel: AccountViewModel, nav: INav, ) { - DisplayUncitedHashtags(event, event.content, callbackUri, nav) + DisplayUncitedHashtags(event, event.content, callbackUri, accountViewModel, nav) } @OptIn(ExperimentalLayoutApi::class) @@ -54,6 +56,7 @@ fun DisplayUncitedHashtags( event: Event, content: String, callbackUri: String? = null, + accountViewModel: AccountViewModel, nav: INav, ) { @Suppress("ProduceStateDoesNotAssignValue") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index d9fb112547..6cebce6135 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -64,6 +64,7 @@ import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.hasGeohashes @@ -177,7 +178,7 @@ open class CommentPostViewModel : override val zapRaiserAmount = mutableStateOf(null) var showRelaysDialog by mutableStateOf(false) - var relayList by mutableStateOf?>(null) + var relayList by mutableStateOf?>(null) fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() @@ -312,11 +313,11 @@ open class CommentPostViewModel : val template = createTemplate() ?: return val relayList = relayList - if (nip95attachments.isNotEmpty() && relayList != null) { + if (nip95attachments.isNotEmpty() && relayList != null && relayList.isNotEmpty()) { val usedImages = template.tags.taggedQuoteIds().toSet() nip95attachments.forEach { - if (usedImages.contains(it.second.id) == true) { - account?.sendNip95Privately(it.first, it.second, relayList) + if (usedImages.contains(it.second.id)) { + account?.sendNip95(it.first, it.second, relayList.toSet()) } } } @@ -554,17 +555,7 @@ open class CommentPostViewModel : fun reloadRelaySet() { val account = accountViewModel?.account ?: return - val nip65 = account.normalizedNIP65WriteRelayList.value - val private = account.normalizedPrivateOutBoxRelaySet.value - val local = account.settings.localRelayServers - - relayList = - if (nip65.isEmpty()) { - account.activeWriteRelays().map { it.url }.toImmutableList() - } else { - val combined: Set = (nip65 + private + local) - combined.toImmutableList() - } + relayList = account.outboxRelays.flow.value.toImmutableList() } fun deleteMediaToUpload(selected: SelectedMediaProcessing) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt index c285b862d0..bfb2c15f8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId @@ -29,11 +30,12 @@ import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId @Composable fun DisplayExternalId( externalId: ExternalId, + accountViewModel: AccountViewModel, nav: INav, ) { when (externalId) { - is GeohashId -> DisplayGeohashExternalId(externalId, nav) - is HashtagId -> DisplayHashtagExternalId(externalId, nav) + is GeohashId -> DisplayGeohashExternalId(externalId, accountViewModel, nav) + is HashtagId -> DisplayHashtagExternalId(externalId, accountViewModel, nav) else -> {} } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayGeoHashExternalId.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayGeoHashExternalId.kt index 4f6a963830..2ac4320e4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayGeoHashExternalId.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayGeoHashExternalId.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.LinkInteractionListener import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withLink @@ -42,6 +43,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier @@ -50,10 +52,21 @@ import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId @Composable fun DisplayGeohashExternalId( externalId: GeohashId, + accountViewModel: AccountViewModel, nav: INav, +) { + DisplayGeohashExternalId(externalId.geohash) { + nav.nav(Route.Geohash(externalId.geohash)) + } +} + +@Composable +fun DisplayGeohashExternalId( + geohash: String, + linkInteractionListener: LinkInteractionListener, ) { Row(modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) { - LoadCityName(externalId.geohash) { cityName -> + LoadCityName(geohash) { cityName -> Icon( imageVector = Icons.Default.LocationOn, contentDescription = stringRes(id = R.string.geohash_exclusive), @@ -67,7 +80,7 @@ fun DisplayGeohashExternalId( text = buildAnnotatedString { withLink( - LinkAnnotation.Clickable("cityname") { nav.nav(Route.Geohash(externalId.geohash)) }, + LinkAnnotation.Clickable("cityname", null, linkInteractionListener), ) { append(cityName) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayHashtagExternalId.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayHashtagExternalId.kt index 8105c2b29b..2b806eb813 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayHashtagExternalId.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayHashtagExternalId.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.LinkInteractionListener import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withLink @@ -41,6 +42,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier @@ -49,7 +51,18 @@ import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId @Composable fun DisplayHashtagExternalId( externalId: HashtagId, + accountViewModel: AccountViewModel, nav: INav, +) { + DisplayHashtagExternalId(externalId.topic) { + nav.nav(Route.Hashtag(externalId.topic)) + } +} + +@Composable +fun DisplayHashtagExternalId( + topic: String, + linkInteractionListener: LinkInteractionListener, ) { Row(modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) { Icon( @@ -65,9 +78,9 @@ fun DisplayHashtagExternalId( text = buildAnnotatedString { withLink( - LinkAnnotation.Clickable("hashtag") { nav.nav(Route.Hashtag(externalId.topic)) }, + LinkAnnotation.Clickable("hashtag", null, linkInteractionListener), ) { - append(externalId.topic) + append(topic) } }, style = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 0fa2e2d664..fecbeabb1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -56,7 +56,7 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialogEasy +import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -210,10 +210,10 @@ fun GenericCommentPostScreen( }, ) { pad -> if (postViewModel.showRelaysDialog) { - RelaySelectionDialogEasy( + RelaySelectionDialog( preSelectedList = postViewModel.relayList ?: persistentListOf(), onClose = { postViewModel.showRelaysDialog = false }, - onPost = { postViewModel.relayList = it.map { it.url }.toImmutableList() }, + onPost = { postViewModel.relayList = it }, accountViewModel = accountViewModel, nav = nav, ) @@ -255,7 +255,7 @@ private fun GenericCommentPostBody( Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) { postViewModel.externalIdentity?.let { Row { - DisplayExternalId(it, nav) + DisplayExternalId(it, accountViewModel, nav) Spacer(modifier = StdVertSpacer) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index febeb1d0d8..9c36dc2e57 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -236,7 +236,7 @@ fun AudioHeader( if (noteEvent.hasHashtags()) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - DisplayUncitedHashtags(noteEvent, callbackUri, nav) + DisplayUncitedHashtags(noteEvent, callbackUri, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index c8b691ddb9..55f6cb25a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -170,6 +170,7 @@ fun LongCommunityHeader( DisplayUncitedHashtags( event = it, content = summary ?: "", + accountViewModel = accountViewModel, nav = nav, ) } @@ -360,9 +361,9 @@ fun WatchAddressableNoteFollows( accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit, ) { - val state by accountViewModel.account.liveKind3Follows.collectAsStateWithLifecycle() + val state by accountViewModel.account.kind3FollowList.flow.collectAsStateWithLifecycle() - onFollowChanges(state.addresses.contains(note.idHex)) + onFollowChanges(state.communities.contains(note.idHex)) } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt index e4c22a0674..844d9e47cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Emoji.kt @@ -185,9 +185,9 @@ private fun EmojiListOptions( CrossfadeIfEnabled(targetState = hasAddedThis, label = "EmojiListOptions", accountViewModel = accountViewModel) { if (it != true) { - AddButton(modifier = Modifier.padding(start = 3.dp)) { accountViewModel.addEmojiPack(usersEmojiList, emojiPackNote) } + AddButton(modifier = Modifier.padding(start = 3.dp)) { accountViewModel.addEmojiPack(emojiPackNote) } } else { - RemoveButton(modifier = Modifier.padding(start = 3.dp)) { accountViewModel.removeEmojiPack(usersEmojiList, emojiPackNote) } + RemoveButton(modifier = Modifier.padding(start = 3.dp)) { accountViewModel.removeEmojiPack(emojiPackNote) } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index bd509b98d8..5ab551357a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -199,6 +199,7 @@ private fun RenderGitPatchEvent( event = noteEvent, content = eventContent, callbackUri = callbackUri, + accountViewModel = accountViewModel, nav = nav, ) } @@ -301,7 +302,7 @@ private fun RenderGitIssueEvent( } if (note.event?.hasHashtags() == true) { - DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, nav) + DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt index a0017ed533..0ef07dc78f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt @@ -73,6 +73,6 @@ fun RenderNIP90ContentDiscoveryResponse( } if (noteEvent.hasHashtags()) { - DisplayUncitedHashtags(noteEvent, noteEvent.content, callbackUri, nav) + DisplayUncitedHashtags(noteEvent, noteEvent.content, callbackUri, accountViewModel, nav) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index 444d76d1f6..7039c77b65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -129,7 +129,7 @@ fun RenderPoll( } if (noteEvent.hasHashtags()) { - DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, nav) + DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index ea568c9634..d82c20c54e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -124,7 +124,7 @@ fun RenderPrivateMessage( } if (noteEvent.hasHashtags()) { - DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, nav) + DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index bdb99d55b9..1f03df636d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -52,8 +52,9 @@ import com.vitorpamplona.amethyst.ui.note.RemoveRelayButton import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent @@ -74,7 +75,7 @@ fun DisplayRelaySet( val relays by remember(noteEvent) { mutableStateOf( - noteEvent.relays().map { RelayBriefInfoCache.RelayBriefInfo(it) }.toImmutableList(), + noteEvent.relays().toImmutableList(), ) } @@ -105,14 +106,14 @@ fun DisplayNIP65RelayList( val writeRelays by remember(baseNote) { mutableStateOf( - noteEvent.writeRelays().map { RelayBriefInfoCache.RelayBriefInfo(it) }.toImmutableList(), + noteEvent.writeRelaysNorm()?.toImmutableList() ?: persistentListOf(), ) } val readRelays by remember(baseNote) { mutableStateOf( - noteEvent.readRelays()?.map { RelayBriefInfoCache.RelayBriefInfo(it) }?.toImmutableList() ?: persistentListOf(), + noteEvent.readRelaysNorm()?.toImmutableList() ?: persistentListOf(), ) } @@ -147,7 +148,7 @@ fun DisplayDMRelayList( val relays by remember(baseNote) { mutableStateOf( - noteEvent.relays().map { RelayBriefInfoCache.RelayBriefInfo(it) }.toImmutableList(), + noteEvent.relays().toImmutableList(), ) } @@ -173,7 +174,7 @@ fun DisplaySearchRelayList( val relays by remember(baseNote) { mutableStateOf( - noteEvent.relays().map { RelayBriefInfoCache.RelayBriefInfo(it) }.toImmutableList(), + noteEvent.relays().toImmutableList(), ) } @@ -189,7 +190,7 @@ fun DisplaySearchRelayList( @Composable fun DisplayRelaySet( - relays: ImmutableList, + relays: ImmutableList, relayListName: String, relayDescription: String?, backgroundColor: MutableState, @@ -239,7 +240,7 @@ fun DisplayRelaySet( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = relay.displayUrl, + text = relay.displayUrl(), fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -250,7 +251,7 @@ fun DisplayRelaySet( ) Column(modifier = Modifier.padding(start = 10.dp)) { - RelayOptionsAction(relay.url, accountViewModel, nav) + RelayOptionsAction(relay, accountViewModel, nav) } } } @@ -274,7 +275,7 @@ fun DisplayRelaySet( @Composable private fun RelayOptionsAction( - relay: String, + relay: NormalizedRelayUrl, accountViewModel: AccountViewModel, nav: INav, ) { @@ -282,11 +283,11 @@ private fun RelayOptionsAction( if (isCurrentlyOnTheUsersList) { AddRelayButton { - nav.nav(Route.EditRelays(relay)) + nav.nav(Route.EditRelays(relay.url)) } } else { RemoveRelayButton { - nav.nav(Route.EditRelays(relay)) + nav.nav(Route.EditRelays(relay.url)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index c9da38a2ed..6f43bdb4d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -163,7 +163,7 @@ fun RenderTextEvent( } if (noteEvent.hasHashtags()) { - DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, nav) + DisplayUncitedHashtags(noteEvent, eventContent, callbackUri, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index 5c008c8cbc..5b47a1473a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -189,7 +189,7 @@ fun VideoDisplay( Row( Modifier.fillMaxWidth(), ) { - DisplayUncitedHashtags(event, summary, callbackUri, nav) + DisplayUncitedHashtags(event, summary, callbackUri, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 98f285f1ee..43a832f8fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -31,19 +31,18 @@ import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.DefaultChannels import com.vitorpamplona.amethyst.model.DefaultDMRelayList import com.vitorpamplona.amethyst.model.DefaultNIP65List +import com.vitorpamplona.amethyst.model.DefaultNIP65RelaySet import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow -import com.vitorpamplona.ammolite.relays.Constants import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip02FollowList.ReadWrite import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent @@ -102,8 +101,6 @@ class AccountStateViewModel : ViewModel() { private suspend fun requestLoginUI() { _accountContent.update { AccountState.LoggedOff } - - viewModelScope.launch(Dispatchers.IO) { Amethyst.instance.serviceManager.pauseAndLogOff() } } suspend fun loginAndStartUI( @@ -298,15 +295,12 @@ class AccountStateViewModel : ViewModel() { backupContactList = ContactListEvent.createFromScratch( followUsers = listOf(ContactTag(keyPair.pubKey.toHexKey(), null, null)), - relayUse = - Constants.defaultRelays.associate { - it.url to ReadWrite(it.read, it.write) - }, + relayUse = emptyMap(), signer = tempSigner, ), backupNIP65RelayList = AdvertisedRelayListEvent.create(DefaultNIP65List, tempSigner), backupDMRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, tempSigner), - backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList, tempSigner), + backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), tempSigner), backupChannelList = ChannelListEvent.create(DefaultChannels, tempSigner), torSettings = TorSettingsFlow.build(torSettings), ) @@ -319,11 +313,14 @@ class AccountStateViewModel : ViewModel() { @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { delay(2000) // waits for the new user to connect to the new relays. - accountSettings.backupUserMetadata?.let { Amethyst.instance.client.send(it) } - accountSettings.backupContactList?.let { Amethyst.instance.client.send(it) } - accountSettings.backupNIP65RelayList?.let { Amethyst.instance.client.send(it) } - accountSettings.backupDMRelayList?.let { Amethyst.instance.client.send(it) } - accountSettings.backupSearchRelayList?.let { Amethyst.instance.client.send(it) } + + val toPost = accountSettings.backupNIP65RelayList?.writeRelaysNorm()?.toSet() ?: DefaultNIP65RelaySet + + accountSettings.backupUserMetadata?.let { Amethyst.instance.client.send(it, toPost) } + accountSettings.backupContactList?.let { Amethyst.instance.client.send(it, toPost) } + accountSettings.backupNIP65RelayList?.let { Amethyst.instance.client.send(it, toPost) } + accountSettings.backupDMRelayList?.let { Amethyst.instance.client.send(it, toPost) } + accountSettings.backupSearchRelayList?.let { Amethyst.instance.client.send(it, toPost) } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt index 28f5749e25..30e4f432c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt @@ -25,11 +25,11 @@ import android.util.Log import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.ALL_FOLLOWS import com.vitorpamplona.amethyst.model.AROUND_ME import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS -import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread @@ -75,7 +75,7 @@ class FollowListState( ) { val kind3Follow = PeopleListOutBoxFeedDefinition( - code = KIND3_FOLLOWS, + code = ALL_FOLLOWS, name = ResourceName(R.string.follow_list_kind3follows), type = CodeNameType.HARDCODED, kinds = DEFAULT_FEED_KINDS, @@ -88,7 +88,6 @@ class FollowListState( name = ResourceName(R.string.follow_list_global), type = CodeNameType.HARDCODED, kinds = DEFAULT_FEED_KINDS, - relays = account.activeGlobalRelays().toList(), ) val aroundMe = @@ -161,11 +160,11 @@ class FollowListState( @OptIn(ExperimentalCoroutinesApi::class) val liveKind3FollowsFlow: Flow> = - account.liveKind3Follows.transformLatest { + account.kind3FollowList.flow.transformLatest { checkNotInMainThread() val communities = - it.addresses.mapNotNull { + it.communities.mapNotNull { LocalCache.checkGetOrCreateAddressableNote(it)?.let { communityNote -> TagFeedDefinition( "Community/${communityNote.idHex}", @@ -174,7 +173,6 @@ class FollowListState( route = Route.Community(communityNote.idHex), kinds = DEFAULT_COMMUNITY_FEEDS, aTags = listOf(communityNote.idHex), - relays = account.activeGlobalRelays().toList(), ) } } @@ -188,7 +186,6 @@ class FollowListState( route = Route.Hashtag(it), kinds = DEFAULT_FEED_KINDS, tTags = listOf(it), - relays = account.activeGlobalRelays().toList(), ) } @@ -201,7 +198,6 @@ class FollowListState( route = Route.Geohash(it), kinds = DEFAULT_FEED_KINDS, gTags = listOf(it), - relays = account.activeGlobalRelays().toList(), ) } @@ -322,7 +318,6 @@ class GlobalFeedDefinition( name: Name, type: CodeNameType, val kinds: List, - val relays: List, ) : FeedDefinition(code, name, type, null) @Immutable @@ -332,7 +327,6 @@ class TagFeedDefinition( type: CodeNameType, route: Route?, val kinds: List, - val relays: List, val pTags: List? = null, val eTags: List? = null, val aTags: List? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 6fc5e8653c..b823834fef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -35,10 +35,10 @@ import androidx.lifecycle.viewmodel.compose.viewModel import coil3.asDrawable import coil3.imageLoader import coil3.request.ImageRequest +import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.collectSuccessfulOperations import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync import com.vitorpamplona.amethyst.isDebug @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.WarningType +import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.service.CashuProcessor import com.vitorpamplona.amethyst.service.CashuToken @@ -90,7 +91,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata -import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser @@ -98,7 +99,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser @@ -112,7 +112,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip47WalletConnect.Response -import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -121,11 +120,11 @@ import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.collectSuccessfulOperations import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf @@ -143,9 +142,9 @@ import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.OkHttpClient @@ -156,25 +155,19 @@ class AccountViewModel( val app: Amethyst, ) : ViewModel(), Dao { - val account = Account(accountSettings, accountSettings.createSigner(), viewModelScope) + val account = Account(accountSettings, accountSettings.createSigner(), app.locationManager.geohashStateFlow, LocalCache, app.client, viewModelScope) val newNotesPreProcessor = PrecacheNewNotesProcessor(account, LocalCache) var firstRoute: Route? = null - // TODO: contact lists are not notes yet - // val kind3Relays: StateFlow = observeByAuthor(ContactListEvent.KIND, account.signer.pubKey) - - val normalizedKind3RelaySetFlow = + val normalizedKind3RelaySetFlow: StateFlow> = account .userProfile() .flow() .relays.stateFlow .map { contactListState -> - checkNotInMainThread() - contactListState.user.latestContactList?.relays()?.map { - RelayUrlFormatter.normalize(it.key) - } ?: emptySet() + contactListState.user.latestContactList?.relays()?.keys ?: emptySet() }.flowOn(Dispatchers.Default) .stateIn( viewModelScope, @@ -182,9 +175,6 @@ class AccountViewModel( emptySet(), ) - val dmRelays: StateFlow = observeByAuthor(ChatMessageRelayListEvent.KIND, account.signer.pubKey) - val searchRelays: StateFlow = observeByAuthor(SearchRelayListEvent.KIND, account.signer.pubKey) - val toastManager = ToastManager() val feedStates = AccountFeedContentStates(this) @@ -322,7 +312,7 @@ class AccountViewModel( fun isNoteAcceptable( note: Note, - accountChoices: Account.LiveHiddenUsers, + accountChoices: HiddenUsersState.LiveHiddenUsers, followUsers: Set, ): NoteComposeReportState { checkNotInMainThread() @@ -363,8 +353,8 @@ class AccountViewModel( fun createIsHiddenFlow(note: Note): StateFlow = noteIsHiddenFlows.get(note) ?: combineTransform( - account.flowHiddenUsers, - account.liveKind3Follows, + account.hiddenUsers.flow, + account.kind3FollowList.flow, note.flow().author(), note.flow().metadata.stateFlow, note.flow().reports.stateFlow, @@ -723,24 +713,18 @@ class AccountViewModel( viewModelScope.launch(Dispatchers.IO) { account.boost(note) } } - fun removeEmojiPack( - usersEmojiList: Note, - emojiList: Note, - ) { - viewModelScope.launch(Dispatchers.IO) { account.removeEmojiPack(usersEmojiList, emojiList) } + fun removeEmojiPack(emojiPack: Note) { + viewModelScope.launch(Dispatchers.IO) { account.removeEmojiPack(emojiPack) } } - fun addEmojiPack( - usersEmojiList: Note, - emojiPack: Note, - ) { - viewModelScope.launch(Dispatchers.IO) { account.addEmojiPack(usersEmojiList, emojiPack) } + fun addEmojiPack(emojiPack: Note) { + viewModelScope.launch(Dispatchers.IO) { account.addEmojiPack(emojiPack) } } fun addMediaToGallery( hex: String, url: String, - relay: String?, + relay: NormalizedRelayUrl?, blurhash: String?, dim: DimensionTag?, hash: String?, @@ -1032,17 +1016,15 @@ class AccountViewModel( } } - fun createRumor(template: EventTemplate) = account.signer.assembleRumor(template) - fun retrieveRelayDocument( - dirtyUrl: String, + relay: NormalizedRelayUrl, onInfo: (Nip11RelayInformation) -> Unit, - onError: (String, Nip11Retriever.ErrorCode, String?) -> Unit, + onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { Nip11CachedRetriever.loadRelayInfo( - dirtyUrl, - okHttpClient = ::okHttpClientForDirty, + relay, + okHttpClient = { okHttpClientForClean(relay) }, onInfo, onError, ) @@ -1152,7 +1134,7 @@ class AccountViewModel( private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? = LocalCache.checkGetOrCreateChannel(key) - suspend fun checkGetOrCreateChannel(key: RoomId): Channel? = LocalCache.checkGetOrCreateChannel(key) + suspend fun checkGetOrCreateChannel(key: RoomId): Channel? = LocalCache.getOrCreateEphemeralChannel(key) fun checkGetOrCreateChannel( key: HexKey, @@ -1284,37 +1266,7 @@ class AccountViewModel( fun setTorSettings(newTorSettings: TorSettings) = viewModelScope.launch(Dispatchers.IO) { - // Only restart relay connections if port or type changes - if (account.settings.setTorSettings(newTorSettings)) { - app.serviceManager.forceRestart() - } - } - - fun forceRestartServices() = - viewModelScope.launch(Dispatchers.IO) { - app.serviceManager.setAccountAndRestart(account) - } - - fun justStart() = - viewModelScope.launch(Dispatchers.IO) { - app.serviceManager.justStartIfItHasAccount() - } - - fun justPause() = - viewModelScope.launch(Dispatchers.IO) { - app.serviceManager.cleanObservers() - app.serviceManager.pauseForGood() - } - - fun pauseAndLogOff() = - viewModelScope.launch(Dispatchers.IO) { - app.serviceManager.cleanObservers() - app.serviceManager.pauseAndLogOff() - } - - fun changeProxyPort(port: Int) = - viewModelScope.launch(Dispatchers.IO) { - app.serviceManager.forceRestart() + account.settings.setTorSettings(newTorSettings) } class Factory( @@ -1556,7 +1508,7 @@ class AccountViewModel( fun okHttpClientForPreview(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForPreviewUrl(url)) - fun okHttpClientForDirty(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForDirty(url)) + fun okHttpClientForClean(url: NormalizedRelayUrl): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForClean(url)) fun okHttpClientForTrustedRelays(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForTrustedRelays()) @@ -1588,7 +1540,7 @@ class AccountViewModel( } fun requestDVMContentDiscovery( - dvmPublicKey: String, + dvmPublicKey: User, onReady: (event: Note) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { @@ -1708,17 +1660,27 @@ class AccountViewModel( fun relayStatusFlow() = app.client.relayStatusFlow() - fun allAccountsSync(): List = - runBlocking { - LocalPreferences.allSavedAccounts().mapNotNull { - try { - it.npub.bechToBytes().toHexKey() - } catch (e: Exception) { - if (e is CancellationException) throw e - null - } + fun convertAccounts(loggedInAccounts: List?): Set { + return loggedInAccounts?.mapNotNull { + try { + it.npub.bechToBytes().toHexKey() + } catch (e: Exception) { + if (e is CancellationException) throw e + null } - } + }?.toSet() ?: emptySet() + } + + val trustedAccounts: StateFlow> = + LocalPreferences.accountsFlow().map { loggedInAccounts -> + convertAccounts(loggedInAccounts) + }.onStart { + emit(convertAccounts(LocalPreferences.allSavedAccounts())) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + emptySet(), + ) val draftNoteCache = CachedDraftNotes(this) @@ -1793,9 +1755,13 @@ class AccountViewModel( val nip19: Nip19Parser.ParseReturn, ) +var mockedCache: AccountViewModel? = null + @SuppressLint("ViewModelConstructorInComposable") @Composable fun mockAccountViewModel(): AccountViewModel { + mockedCache?.let { return it } + val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel() sharedPreferencesViewModel.init() @@ -1811,12 +1777,18 @@ fun mockAccountViewModel(): AccountViewModel { ), sharedPreferencesViewModel.sharedPrefs, Amethyst(), - ) + ).also { + mockedCache = it + } } +var vitorCache: AccountViewModel? = null + @SuppressLint("ViewModelConstructorInComposable") @Composable fun mockVitorAccountViewModel(): AccountViewModel { + mockedCache?.let { return it } + val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel() sharedPreferencesViewModel.init() @@ -1830,5 +1802,7 @@ fun mockVitorAccountViewModel(): AccountViewModel { ), sharedPreferencesViewModel.sharedPrefs, Amethyst(), - ) + ).also { + vitorCache = it + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index c40608f6ca..2c6389ec22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -54,12 +54,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.Chat import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus -import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable @@ -88,6 +85,9 @@ fun LoggedInPage( // Sets up Coil's Image Loader ObserveImageLoadingTor(accountViewModel) + // Sets up the use of Proxy based on this Account's settings + SetProxyDeterminator(accountViewModel) + // Loads account information + DMs and Notifications from Relays. AccountFilterAssemblerSubscription(accountViewModel) @@ -103,10 +103,8 @@ fun LoggedInPage( // Updates local cache of the anti-spam filter choice of this user. ObserveAntiSpamFilterSettings(accountViewModel) - ManageRelayServices(accountViewModel, sharedPreferencesViewModel) - - // Turns Embed Tor on if needed. - ManageTorInstance(accountViewModel) + // Pauses relay services when the app pauses + ManageRelayServices(accountViewModel) // Listens to Amber ListenToExternalSignerIfNeeded(accountViewModel) @@ -129,66 +127,24 @@ fun ObserveAntiSpamFilterSettings(accountViewModel: AccountViewModel) { Amethyst.instance.cache.antiSpam.active = isSpamActive } +@Composable +fun SetProxyDeterminator(accountViewModel: AccountViewModel) { + LaunchedEffect(accountViewModel) { + Amethyst.instance.torProxySettingsAnchor.flow.tryEmit(accountViewModel.account.torRelayState.flow) + } +} + @Composable fun ObserveImageLoadingTor(accountViewModel: AccountViewModel) { - LaunchedEffect(Unit) { + LaunchedEffect(accountViewModel) { Amethyst.instance.setImageLoader(accountViewModel.account::shouldUseTorForImageDownload) } } @Composable -fun ManageRelayServices( - accountViewModel: AccountViewModel, - sharedPreferencesViewModel: SharedPreferencesViewModel, -) { - LaunchedEffect( - sharedPreferencesViewModel.sharedPrefs.currentNetworkId, - sharedPreferencesViewModel.sharedPrefs.isOnMobileOrMeteredConnection, - ) { - Log.d("ManageRelayServices", "Loading/Change Network Id/State ${sharedPreferencesViewModel.sharedPrefs.currentNetworkId}, forcing start/restart of the relay services") - accountViewModel.forceRestartServices() - } - - val lifeCycleOwner = LocalLifecycleOwner.current - - val scope = rememberCoroutineScope() - var job = remember { null } - - Log.d("ManageRelayServices", "Job $job for $accountViewModel") - - DisposableEffect(key1 = accountViewModel) { - job?.cancel() - val observer = - LifecycleEventObserver { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> { - job?.cancel() - Log.d("ManageRelayServices", "Resuming Relay Services $accountViewModel") - job = accountViewModel.justStart() - } - Lifecycle.Event.ON_PAUSE -> { - Log.d("ManageRelayServices", "Prepare to pause Relay Services $accountViewModel") - job?.cancel() - job = - scope.launch { - delay(30000) // 30 seconds - Log.d("ManageRelayServices", "Pausing Relay Services $accountViewModel") - accountViewModel.justPause() - } - } - else -> {} - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { - job?.cancel() - lifeCycleOwner.lifecycle.removeObserver(observer) - Log.d("ManageRelayServices", "Disposing Relay Services $accountViewModel") - // immediately stops upon disposal - accountViewModel.pauseAndLogOff() - } - } +fun ManageRelayServices(accountViewModel: AccountViewModel) { + val relayServices by Amethyst.instance.relayProxyClientConnector.relayServices.collectAsStateWithLifecycle() + Log.d("ManageRelayServices", "Relay Services changed $relayServices") } @Composable @@ -210,28 +166,6 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) { } } -@Composable -fun ManageTorInstance(accountViewModel: AccountViewModel) { - val torSettings by accountViewModel.account.settings.torSettings.torType - .collectAsStateWithLifecycle() - if (torSettings == TorType.INTERNAL) { - WatchTorConnection(accountViewModel) - } -} - -@Composable -fun WatchTorConnection(accountViewModel: AccountViewModel) { - val status by Amethyst.instance.torManager.status - .collectAsStateWithLifecycle() - - if (status is TorServiceStatus.Active) { - LaunchedEffect(key1 = status, key2 = accountViewModel) { - Log.d("TorService", "Tor has just finished connecting, force restart relays $accountViewModel") - accountViewModel.changeProxyPort((status as TorServiceStatus.Active).port) - } - } -} - @Composable private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) { if (accountViewModel.account.signer is NostrSignerExternal) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index e0a9b04050..e63a68a008 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -233,7 +233,7 @@ fun NormalChatNote( val geo = remember(note) { note.event?.geoHashOrScope() } if (geo != null) { Spacer(StdHorzSpacer) - DisplayLocation(geo, nav) + DisplayLocation(geo, accountViewModel, nav) } val pow = remember(note) { note.event?.strongPoWOrNull() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt index 0dfaa6f9a7..aef777bed0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.Nip11Retriever @@ -68,14 +69,14 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter +import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm @Composable fun RenderCreateChannelNote( @@ -110,11 +111,11 @@ fun RenderChannelDataPreview() { id = "bbaacc", uri = "nostr:nevent1...", channelInfo = - ChannelData( + ChannelDataNorm( "My Group", "Testing About me", "http://test.com", - listOf("wss://nostr.mom", "wss://nos.lol"), + listOf(Constants.mom, Constants.nos), ), tags = EmptyTagList, bgColor = remember { mutableStateOf(Color.Transparent) }, @@ -128,7 +129,7 @@ fun RenderChannelDataPreview() { fun RenderChannelData( id: HexKey, uri: String, - channelInfo: ChannelData, + channelInfo: ChannelDataNorm, tags: ImmutableListOfLists, bgColor: MutableState, accountViewModel: AccountViewModel, @@ -233,26 +234,21 @@ fun RenderRelayLinePreview() { @OptIn(ExperimentalFoundationApi::class) @Composable fun RenderRelayLinePublicChat( - dirtyUrl: String, + relay: NormalizedRelayUrl, accountViewModel: AccountViewModel, nav: INav, ) { @Suppress("ProduceStateDoesNotAssignValue") - val relayInfo by loadRelayInfo(dirtyUrl, accountViewModel) + val relayInfo by loadRelayInfo(relay, accountViewModel) var openRelayDialog by remember { mutableStateOf(false) } - val info = - remember(dirtyUrl) { - RelayBriefInfoCache.get(RelayUrlFormatter.normalize(dirtyUrl)) - } - if (openRelayDialog) { relayInfo?.let { RelayInformationDialog( onClose = { openRelayDialog = false }, relayInfo = it, - relayBriefInfo = info, + relay = relay, accountViewModel = accountViewModel, nav = nav, ) @@ -261,18 +257,18 @@ fun RenderRelayLinePublicChat( val clipboardManager = LocalClipboardManager.current val clickableModifier = - remember(dirtyUrl) { + remember(relay) { Modifier.combinedClickable( onLongClick = { - clipboardManager.setText(AnnotatedString(dirtyUrl)) + clipboardManager.setText(AnnotatedString(relay.url)) }, onClick = { accountViewModel.retrieveRelayDocument( - dirtyUrl, + relay = relay, onInfo = { openRelayDialog = true }, - onError = { url, errorCode, exceptionMessage -> + onError = { relay, errorCode, exceptionMessage -> accountViewModel.toastManager.toast( R.string.unable_to_download_relay_document, when (errorCode) { @@ -288,7 +284,7 @@ fun RenderRelayLinePublicChat( Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> R.string.relay_information_document_error_failed_with_http }, - url, + relay.url, exceptionMessage ?: errorCode.toString(), ) }, @@ -298,8 +294,8 @@ fun RenderRelayLinePublicChat( } RenderRelayLine( - info.displayUrl, - relayInfo?.icon ?: info.favIcon, + relay.displayUrl(), + relayInfo?.icon, clickableModifier, showPicture = accountViewModel.settings.showProfilePictures.value, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt index 8d54bdbe12..2ba8587ca1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey // This allows multiple screen to be listening to tags, even the same tag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt index e10f139e11..04469d6111 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt @@ -21,9 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient class ChatroomFilterSubAssembler( client: NostrClient, @@ -31,13 +30,8 @@ class ChatroomFilterSubAssembler( ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: ChatroomQueryState, - since: Map?, - ): List? = - filterNip04DMs( - key.room.users, - key.account.userProfile().pubkeyHex, - since, - ) + since: SincePerRelayMap?, + ) = filterNip04DMs(key.room.users, key.account, since) override fun user(key: ChatroomQueryState) = key.account.userProfile() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt index 3d67904609..e36eb8247a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt @@ -20,41 +20,75 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import kotlin.collections.ifEmpty import kotlin.collections.toList fun filterNip04DMs( group: Set?, - user: HexKey?, - since: Map?, -): List? { - if (group == null || group.isEmpty() || user == null || user.isEmpty()) return null + account: Account?, + since: SincePerRelayMap?, +): List? { + if (group == null || group.isEmpty() || account == null) return null - return listOf( - TypedFilter( - types = setOf(FeedType.PRIVATE_DMS), + val userOutboxRelays = account.outboxRelays.flow.value + val userInboxRelays = account.dmRelays.flow.value + + val groupOutboxRelays = mutableSetOf() + val groupInboxRelays = mutableSetOf() + + group.forEach { + val authorHomeRelayEventAddress = AdvertisedRelayListEvent.createAddressTag(it) + val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) + + val outbox = + authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null } + ?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null } + ?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay) + + groupOutboxRelays.addAll(outbox) + + val inbox = + authorHomeRelayEvent?.readRelaysNorm()?.ifEmpty { null } + ?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null } + ?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay) + + groupInboxRelays.addAll(inbox) + } + + val toMeRelays = (userInboxRelays + groupOutboxRelays) + val fromMeRelays = (userOutboxRelays + groupInboxRelays) + + return toMeRelays.map { + RelayBasedFilter( + relay = it, filter = - SincePerRelayFilter( + Filter( kinds = listOf(PrivateDmEvent.KIND), authors = group.toList(), - tags = mapOf("p" to listOf(user)), - since = since, + tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)), + since = since?.get(it)?.time, ), - ), - TypedFilter( - types = setOf(FeedType.PRIVATE_DMS), - filter = - SincePerRelayFilter( - kinds = listOf(PrivateDmEvent.KIND), - authors = listOf(user), - tags = mapOf("p" to group.toList()), - since = since, - ), - ), - ) + ) + } + + fromMeRelays.map { + RelayBasedFilter( + relay = it, + filter = + Filter( + kinds = listOf(PrivateDmEvent.KIND), + authors = listOf(account.userProfile().pubkeyHex), + tags = mapOf("p" to group.toList()), + since = since?.get(it)?.time, + ), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index e20c597ecb..196c7dc455 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -622,7 +622,7 @@ class ChatNewMessageViewModel : toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item) updateRoomFromUsersInput() - val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(item.pubkeyHex))?.event as? AdvertisedRelayListEvent)?.readRelays() + val relayList = (LocalCache.getAddressableNoteIfExists(AdvertisedRelayListEvent.createAddressTag(item.pubkeyHex))?.event as? AdvertisedRelayListEvent)?.readRelaysNorm() nip17 = relayList != null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt index 79790d8ac6..b0faabfc67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelFromUserFilterSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelPublicFilterSubAssembler -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to tags, even the same tag class ChannelQueryState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt index 1d0374ea21..d9f72f4dc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt @@ -24,10 +24,10 @@ import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ChannelFromUserFilterSubAssembler( client: NostrClient, @@ -35,8 +35,8 @@ class ChannelFromUserFilterSubAssembler( ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: ChannelQueryState, - since: Map?, - ): List? = + since: SincePerRelayMap?, + ): List? = when (val channel = key.channel) { is EphemeralChatChannel -> filterMyMessagesToEphemeralChat(channel, userHex(key), since) is PublicChatChannel -> filterMyMessagesToPublicChat(channel, user(key).pubkeyHex, since) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt index 3c3a38263e..31455be31a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt @@ -24,10 +24,10 @@ import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ChannelPublicFilterSubAssembler( client: NostrClient, @@ -35,8 +35,8 @@ class ChannelPublicFilterSubAssembler( ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: ChannelQueryState, - since: Map?, - ): List? = + since: SincePerRelayMap?, + ): List? = when (val channel = key.channel) { is EphemeralChatChannel -> filterMessagesToEphemeralChat(channel, since) is PublicChatChannel -> filterMessagesToPublicChat(channel, since) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt index 849650b0a6..fc208afdf0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt @@ -21,22 +21,20 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter fun filterMessagesToEphemeralChat( channel: EphemeralChatChannel, - since: Map?, -): List? = - listOf( - TypedFilter( - types = - setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL), + since: SincePerRelayMap?, +): List = + channel.relays().toSet().map { + RelayBasedFilter( + relay = it, filter = - SincePerRelayFilter( + Filter( kinds = listOf(EphemeralChatEvent.KIND), tags = if (channel.roomId.id.isBlank()) { @@ -45,7 +43,7 @@ fun filterMessagesToEphemeralChat( mapOf("d" to listOfNotNull(channel.roomId.id)) }, limit = 200, - since = since, + since = since?.get(it)?.time, ), - ), - ) + ) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt index c3177484c9..f11426e9e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt @@ -21,25 +21,24 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent fun filterMessagesToLiveActivities( channel: LiveActivitiesChannel, - since: Map?, -): List? = - listOf( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), + since: SincePerRelayMap?, +): List = + channel.relays().toSet().map { + RelayBasedFilter( + relay = it, filter = - SincePerRelayFilter( + Filter( kinds = listOf(LiveActivitiesChatMessageEvent.KIND), tags = mapOf("a" to listOfNotNull(channel.idHex)), limit = 200, - since = since, + since = since?.get(it)?.time, ), - ), - ) + ) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt index 046b7a0264..61c5bcf30e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt @@ -21,25 +21,24 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent fun filterMessagesToPublicChat( channel: PublicChatChannel, - since: Map?, -): List? = - listOf( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), + since: SincePerRelayMap?, +): List? = + channel.relays().toSet().map { + RelayBasedFilter( + relay = it, filter = - SincePerRelayFilter( + Filter( kinds = listOf(ChannelMessageEvent.KIND), tags = mapOf("e" to listOfNotNull(channel.idHex)), limit = 200, - since = since, + since = since?.get(it)?.time, ), - ), - ) + ) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt index 4e290248db..0727ccda94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt @@ -21,24 +21,22 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter fun filterMyMessagesToEphemeralChat( channel: EphemeralChatChannel, pubKey: HexKey, - since: Map?, -): List? = - listOf( - TypedFilter( - types = - setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL), + since: SincePerRelayMap?, +): List = + channel.relays().toSet().map { + RelayBasedFilter( + relay = it, filter = - SincePerRelayFilter( + Filter( kinds = listOf(EphemeralChatEvent.KIND), tags = if (channel.roomId.id.isBlank()) { @@ -48,7 +46,7 @@ fun filterMyMessagesToEphemeralChat( }, authors = listOf(pubKey), limit = 50, - since = since, + since = since?.get(it)?.time, ), - ), - ) + ) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt index 40887b40ed..509b113340 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt @@ -21,28 +21,27 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent fun filterMyMessagesToLiveActivities( channel: LiveActivitiesChannel, pubKey: HexKey, - since: Map?, -): List? = - listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH), + since: SincePerRelayMap?, +): List? = + channel.relays().toSet().map { + RelayBasedFilter( + relay = it, filter = - SincePerRelayFilter( + Filter( kinds = listOf(LiveActivitiesChatMessageEvent.KIND), tags = mapOf("a" to listOfNotNull(channel.idHex)), authors = listOf(pubKey), limit = 50, - since = since, + since = since?.get(it)?.time, ), - ), - ) + ) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt index 25f7660c00..681e36aafc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt @@ -21,28 +21,27 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent fun filterMyMessagesToPublicChat( channel: PublicChatChannel, pubKey: HexKey, - since: Map?, -): List? = - listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH), + since: SincePerRelayMap?, +): List = + channel.relays().toSet().map { + RelayBasedFilter( + relay = it, filter = - SincePerRelayFilter( + Filter( kinds = listOf(ChannelMessageEvent.KIND), tags = mapOf("e" to listOfNotNull(channel.idHex)), authors = listOf(pubKey), limit = 50, - since = since, + since = since?.get(it)?.time, ), - ), - ) + ) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt index 17c205572e..6550a5f6df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt @@ -50,28 +50,24 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemC import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.ui.theme.Size35dp -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter @Composable fun loadRelayInfo( - relayUrl: String, + relay: NormalizedRelayUrl, accountViewModel: AccountViewModel, ): State { @Suppress("ProduceStateDoesNotAssignValue") val relayInfo = produceStateIfNotNull( - Nip11CachedRetriever.getFromCache(relayUrl), - relayUrl, + Nip11CachedRetriever.getFromCache(relay), + relay, ) { accountViewModel.retrieveRelayDocument( - relayUrl, - onInfo = { - value = it - }, - onError = { url, errorCode, exceptionMessage -> - }, + relay = relay, + onInfo = { value = it }, + onError = { url, errorCode, exceptionMessage -> }, ) } @@ -124,14 +120,10 @@ private fun DrawRelayIcon( accountViewModel: AccountViewModel, ) { val relayInfo by loadRelayInfo(channel.roomId.relayUrl, accountViewModel) - val info = - remember(channel.roomId.relayUrl) { - RelayBriefInfoCache.get(RelayUrlFormatter.normalize(channel.roomId.relayUrl)) - } RobohashFallbackAsyncImage( robot = channel.idHex, - model = relayInfo?.icon ?: info.favIcon, + model = relayInfo?.icon, contentDescription = stringRes(R.string.profile_image), contentScale = ContentScale.Crop, modifier = HeaderPictureModifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt index 7cfd03779f..82fb3225e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer class NewEphemeralChatMetaViewModel : ViewModel() { private var account: Account? = null @@ -42,7 +43,9 @@ class NewEphemeralChatMetaViewModel : ViewModel() { this.account = account } - fun buildRoom() = RoomId(channelName.value.text, relayUrl.value.text) + fun buildRoom() = + RelayUrlNormalizer.normalizeOrNull(relayUrl.value.text) + ?.let { RoomId(channelName.value.text, it) } /* fun createOrUpdate(onDone: (RoomId) -> Unit) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt index e6ec5d2360..2a8cb36583 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt @@ -124,8 +124,10 @@ private fun ChannelMetadataScaffold( JoinButton( onPost = { - nav.popBack() - nav.nav(routeFor(postViewModel.buildRoom())) + postViewModel.buildRoom()?.let { + nav.popBack() + nav.nav(routeFor(it)) + } }, postViewModel.canPost, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index 3814ef2fd4..b5a872aee5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -229,7 +229,7 @@ private fun ChannelMetadataScaffold( ) } - itemsIndexed(feedState, key = { _, item -> "ChatRelays" + item.url }) { index, item -> + itemsIndexed(feedState, key = { _, item -> "ChatRelays" + item.relay }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteHomeRelay(item) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 91953ca272..ff1c0ab197 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -84,7 +84,7 @@ class ChannelMetadataViewModel : ViewModel() { val relays = channel.info.relays ?.map { relaySetupInfoBuilder(it) } - ?.distinctBy { it.url } + ?.distinctBy { it.relay } _channelRelays.update { relays ?: emptyList() } } @@ -102,19 +102,17 @@ class ChannelMetadataViewModel : ViewModel() { channelName.value.text, channelDescription.value.text, channelPicture.value.text, - channelRelays.value.map { it.url }, + channelRelays.value.map { it.relay }, ) account.signAndSendPrivatelyOrBroadcast( template, relayList = { it.channelInfo().relays }, onDone = { - val channel = LocalCache.getOrCreateChannel(it.id) { PublicChatChannel(it) } - if (channel is PublicChatChannel) { - // follows the channel - account.follow(channel) - onDone(channel) - } + val channel = LocalCache.getOrCreatePublicChatChannel(it.id) + // follows the channel + account.follow(channel) + onDone(channel) }, ) } else { @@ -128,7 +126,7 @@ class ChannelMetadataViewModel : ViewModel() { channelName.value.text, channelDescription.value.text, channelPicture.value.text, - channelRelays.value.map { it.url }, + channelRelays.value.map { it.relay }, hint, ) } else { @@ -138,7 +136,7 @@ class ChannelMetadataViewModel : ViewModel() { channelName.value.text, channelDescription.value.text, channelPicture.value.text, - channelRelays.value.map { it.url }, + channelRelays.value.map { it.relay }, eTag, ) } @@ -147,10 +145,8 @@ class ChannelMetadataViewModel : ViewModel() { template, relayList = { it.channelInfo().relays }, onDone = { - val channel = LocalCache.getOrCreateChannel(it.id) { PublicChatChannel(it) } - if (channel is PublicChatChannel) { - onDone(channel) - } + val channel = LocalCache.getOrCreatePublicChatChannel(it.id) + onDone(channel) }, ) } @@ -161,7 +157,7 @@ class ChannelMetadataViewModel : ViewModel() { } fun addHomeRelay(relay: BasicRelaySetupInfo) { - if (_channelRelays.value.any { it.url == relay.url }) return + if (_channelRelays.value.any { it.relay == relay.relay }) return _channelRelays.update { it.plus(relay) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt index 5ef665f554..e696c45978 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt @@ -109,7 +109,7 @@ fun LongLiveActivityChannelHeader( if (summary != null) { baseChannel.info?.let { if (it.hasHashtags()) { - DisplayUncitedHashtags(it, summary, nav) + DisplayUncitedHashtags(it, summary, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index a2b3b03a3c..18b0fa8222 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -273,7 +273,7 @@ open class ChannelNewMessageViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val channelRelays = channel?.relays() ?: emptyList() + val channelRelays = channel?.relays() ?: emptySet() accountViewModel?.account?.signAndSendPrivately(template, channelRelays) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index a50d7de059..01f31b15bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -82,14 +82,12 @@ import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter @Composable fun ChatroomHeaderCompose( @@ -229,10 +227,6 @@ private fun ChannelRoomCompose( val channelState by observeChannel(channel, accountViewModel) val relayInfo by loadRelayInfo(channel.roomId.relayUrl, accountViewModel) - val info = - remember(channel.roomId.relayUrl) { - RelayBriefInfoCache.get(RelayUrlFormatter.normalize(channel.roomId.relayUrl)) - } val channelName = channelState?.channel?.toBestDisplayName() ?: channel.toBestDisplayName() @@ -253,7 +247,7 @@ private fun ChannelRoomCompose( ChannelName( channelIdHex = channel.idHex, - channelPicture = relayInfo?.icon ?: info.favIcon, + channelPicture = relayInfo?.icon, channelTitle = { modifier -> ChannelTitleWithLabelInfo(channelName, R.string.ephemeral_relay_chat, modifier) }, channelLastTime = note.createdAt(), channelLastContent = "$authorName: $description", diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index b691d47010..83f10b485f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -57,7 +57,7 @@ class ChatroomListKnownFeedFilter( val publicChannels = account - .publicChatList.livePublicChatList.value + .publicChatList.flow.value .mapNotNull { LocalCache.getChannelIfExists(it.eventId) } .mapNotNull { it -> it.notes @@ -168,7 +168,7 @@ class ChatroomListKnownFeedFilter( newItems: Set, account: Account, ): MutableMap { - val followingChannels = account.publicChatList.livePublicChatEventIdSet.value + val followingChannels = account.publicChatList.flowSet.value val newRelevantPublicMessages = mutableMapOf() newItems .filter { it.event is ChannelMessageEvent } @@ -201,7 +201,7 @@ class ChatroomListKnownFeedFilter( if (noteEvent != null) { val room = noteEvent.roomId() - if (room in followingEphemeralChats && account.isAcceptable(newNote)) { + if (room != null && room in followingEphemeralChats && account.isAcceptable(newNote)) { val lastNote = newRelevantEphemeralChats.get(room) if (lastNote != null) { if ((newNote.createdAt() ?: 0) > (lastNote.createdAt() ?: 0)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt index 13dbf03f51..4649f1c2bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to tags, even the same tag class ChatroomListState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt index 134c88999b..844b1f67d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt @@ -20,10 +20,18 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlin.collections.forEach class DMsFromUserFilterSubAssembler( client: NostrClient, @@ -31,11 +39,45 @@ class DMsFromUserFilterSubAssembler( ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( key: ChatroomListState, - since: Map?, - ): List? = - listOfNotNull( - filterNip04DMsToAndFromMe(key.account.userProfile().pubkeyHex, since), - ).flatten() + since: SincePerRelayMap?, + ): List? = + key.account.outboxRelays.flow.value.map { + filterNip04DMsFromMe(key.account.userProfile(), it, since?.get(it)?.time) + } + + key.account.dmRelays.flow.value.map { + filterNip04DMsToMe(key.account.userProfile(), it, since?.get(it)?.time) + } override fun user(key: ChatroomListState) = key.account.userProfile() + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: ChatroomListState): Subscription { + val user = user(key) + userJobMap[user]?.forEach { it.cancel() } + userJobMap[user] = + listOf( + key.account.scope.launch(Dispatchers.Default) { + key.account.outboxRelays.flow.collectLatest { + invalidateFilters() + } + }, + key.account.scope.launch(Dispatchers.Default) { + key.account.dmRelays.flow.collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingEphemeralChats.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingEphemeralChats.kt index 111b96f05d..1c1c68f193 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingEphemeralChats.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingEphemeralChats.kt @@ -20,39 +20,44 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.Constants +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.utils.mapOfSet fun filterFollowingEphemeralChats( - followingChannels: Set?, - since: Map?, -): List? { - if (followingChannels == null || followingChannels.isEmpty()) return null + followingChannels: Set, + since: SincePerRelayMap?, +): List? { + if (followingChannels.isEmpty()) return null - val dTags = - followingChannels.map { - if (it.id.isBlank()) { - "_" - } else { - it.id + val relayRoomDTags = + mapOfSet { + followingChannels.forEach { room -> + if (!room.relayUrl.url.isBlank()) { + add(room.relayUrl, room.id.ifBlank { "_" }) + } else { + Constants.eventFinderRelays.forEach { + add(it, room.id.ifBlank { "_" }) + } + } } } - return listOf( - TypedFilter( + return relayRoomDTags.map { + RelayBasedFilter( // Metadata comes from any relay - types = setOf(FeedType.PUBLIC_CHATS), + relay = it.key, filter = - SincePerRelayFilter( + Filter( kinds = listOf(EphemeralChatEvent.KIND), - tags = mapOf("d" to dTags), + tags = mapOf("d" to it.value.sorted()), limit = 100, - since = since, + since = since?.get(it.key)?.time, ), - ), - ) + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt index acc4857427..e92020f0d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt @@ -20,29 +20,44 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.Constants +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import kotlin.collections.toList +import com.vitorpamplona.quartz.utils.mapOfSet fun filterFollowingPublicChats( - followingChannels: Set?, - since: Map?, -): List? { - if (followingChannels == null || followingChannels.isEmpty()) return null + followingChannels: Set, + since: SincePerRelayMap?, +): List? { + if (followingChannels.isEmpty()) return null - return listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, + val relayRoomDTags = + mapOfSet { + followingChannels.forEach { channelId -> + val relays = + LocalCache.getChannelIfExists(channelId)?.relays() + ?: LocalCache.relayHints.hintsForEvent(channelId).ifEmpty { null } + ?: Constants.eventFinderRelays + + relays.forEach { relayUrl -> + add(relayUrl, channelId) + } + } + } + + return relayRoomDTags.map { + RelayBasedFilter( + relay = it.key, filter = - SincePerRelayFilter( + Filter( kinds = listOf(ChannelCreateEvent.KIND), - ids = followingChannels.toList(), - since = since, + ids = it.value.sorted(), + since = since?.get(it.key)?.time, ), - ), - ) + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt index 7fe0b34d4f..59b1288c7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt @@ -20,44 +20,60 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.Constants +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import kotlin.collections.toList +import com.vitorpamplona.quartz.utils.mapOfSet fun filterLastMessageFollowingPublicChats( - followingChannels: Set?, - since: Map?, -): List? { - if (followingChannels == null || followingChannels.isEmpty()) return null + followingChannels: Set, + since: SincePerRelayMap?, +): List? { + if (followingChannels.isEmpty()) return null - return listOf( - TypedFilter( - // Metadata comes from any relay - types = EVENT_FINDER_TYPES, - filter = - SincePerRelayFilter( - kinds = listOf(ChannelMetadataEvent.KIND), - tags = mapOf("e" to followingChannels.toList()), - since = since, - limit = 1, - ), - ), - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SincePerRelayFilter( - kinds = listOf(ChannelMessageEvent.KIND), - tags = mapOf("e" to followingChannels.toList()), - since = since, - // Remember to consider spam that is being removed from the UI - limit = 100, - ), - ), - ) + val relayRoomDTags = + mapOfSet { + followingChannels.forEach { channelId -> + val relays = + LocalCache.getChannelIfExists(channelId)?.relays() + ?: LocalCache.relayHints.hintsForEvent(channelId).ifEmpty { null } + ?: Constants.eventFinderRelays + + relays.forEach { relayUrl -> + add(relayUrl, channelId) + } + } + } + + return relayRoomDTags.map { + listOf( + RelayBasedFilter( + // Metadata comes from any relay + relay = it.key, + filter = + Filter( + kinds = listOf(ChannelMetadataEvent.KIND), + tags = mapOf("e" to it.value.sorted()), + since = since?.get(it.key)?.time, + limit = 1, + ), + ), + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = listOf(ChannelMessageEvent.KIND), + tags = mapOf("e" to it.value.sorted()), + since = since?.get(it.key)?.time, + // Remember to consider spam that is being removed from the UI + limit = 100, + ), + ), + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt new file mode 100644 index 0000000000..afadd3b355 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent + +fun filterNip04DMsFromMe( + user: User, + relay: NormalizedRelayUrl, + since: Long?, +): RelayBasedFilter { + return RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(PrivateDmEvent.KIND), + authors = listOf(user.pubkeyHex), + since = since, + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToAndFromMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToAndFromMe.kt deleted file mode 100644 index 413bbdab2a..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToAndFromMe.kt +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource - -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent - -fun filterNip04DMsToAndFromMe( - user: HexKey?, - since: Map?, -): List? { - if (user == null || user.isEmpty()) return null - - return listOf( - TypedFilter( - types = setOf(FeedType.PRIVATE_DMS), - filter = - SincePerRelayFilter( - kinds = listOf(PrivateDmEvent.KIND), - tags = mapOf("p" to listOf(user)), - since = since, - ), - ), - TypedFilter( - types = setOf(FeedType.PRIVATE_DMS), - filter = - SincePerRelayFilter( - kinds = listOf(PrivateDmEvent.KIND), - authors = listOf(user), - since = since, - ), - ), - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt new file mode 100644 index 0000000000..b86aa6280b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent + +fun filterNip04DMsToMe( + user: User, + relay: NormalizedRelayUrl, + since: Long?, +): RelayBasedFilter { + return RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(PrivateDmEvent.KIND), + tags = mapOf("p" to listOf(user.pubkeyHex)), + since = since, + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt index b1dfcbd56b..8d3595b267 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job @@ -40,8 +40,8 @@ class FollowingEphemeralChatSubAssembler( ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( key: ChatroomListState, - since: Map?, - ): List? = + since: SincePerRelayMap?, + ): List? = listOfNotNull( filterFollowingEphemeralChats(key.account.ephemeralChatList.liveEphemeralChatList.value, since), ).flatten() @@ -69,7 +69,7 @@ class FollowingEphemeralChatSubAssembler( key: User, subId: String, ) { - return super.endSub(key, subId) + super.endSub(key, subId) userJobMap[key]?.forEach { it.cancel() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt index dc5e035257..61b3de874f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job @@ -40,11 +40,11 @@ class FollowingPublicChatSubAssembler( ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( key: ChatroomListState, - since: Map?, - ): List? = + since: SincePerRelayMap?, + ): List? = listOfNotNull( - filterLastMessageFollowingPublicChats(key.account.publicChatList.livePublicChatEventIdSet.value, since), - filterFollowingPublicChats(key.account.publicChatList.livePublicChatEventIdSet.value, since), + filterLastMessageFollowingPublicChats(key.account.publicChatList.flowSet.value, since), + filterFollowingPublicChats(key.account.publicChatList.flowSet.value, since), ).flatten() override fun user(key: ChatroomListState) = key.account.userProfile() @@ -57,7 +57,7 @@ class FollowingPublicChatSubAssembler( userJobMap[key.account.userProfile()] = listOf( key.account.scope.launch(Dispatchers.Default) { - key.account.publicChatList.livePublicChatEventIdSet.sample(5000).collectLatest { + key.account.publicChatList.flowSet.sample(5000).collectLatest { invalidateFilters() } }, @@ -70,7 +70,7 @@ class FollowingPublicChatSubAssembler( key: User, subId: String, ) { - return super.endSub(key, subId) + super.endSub(key, subId) userJobMap[key]?.forEach { it.cancel() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt index 3a84eea9cf..b7065770a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt @@ -148,7 +148,7 @@ private fun ImageVideoPostChat( fileUploadState: ChatFileUploadState, accountViewModel: AccountViewModel, ) { - val fileServers by accountViewModel.account.liveServerList.collectAsState() + val fileServers by accountViewModel.account.serverLists.liveServerList.collectAsState() val fileServerOptions = remember(fileServers) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt index a9d06b3041..bb630d892d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt @@ -20,11 +20,13 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import kotlin.collections.ifEmpty class CommunityFeedFilterSubAssembler( client: NostrClient, @@ -32,16 +34,23 @@ class CommunityFeedFilterSubAssembler( ) : SingleSubEoseManager(client, allKeys) { override fun updateFilter( keys: List, - since: Map?, - ): List? { + since: SincePerRelayMap?, + ): List { if (keys.isEmpty()) return emptyList() - return keys.mapNotNull { + return keys.flatMap { val commEvent = it.community.event if (commEvent is CommunityDefinitionEvent) { - filterCommunityPosts(commEvent, since) + val relays = + commEvent.relayUrls().ifEmpty { null } + ?: LocalCache.relayHints.hintsForAddress(commEvent.addressTag()).ifEmpty { null } + ?: it.community.relayUrls() + + relays.toSet().map { + filterCommunityPosts(it, commEvent, since?.get(it)?.time) + } } else { - null + emptyList() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt index a9ec56023d..bc9087f18f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to tags, even the same tag class CommunityQueryState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt index 7cd506d89d..e0447aae2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt @@ -20,25 +20,26 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent fun filterCommunityPosts( + relay: NormalizedRelayUrl, community: CommunityDefinitionEvent, - since: Map?, -): TypedFilter = - TypedFilter( - types = COMMON_FEED_TYPES, + since: Long?, +): RelayBasedFilter { + return RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( + Filter( authors = community.moderators().map { it.pubKey }.plus(community.pubKey), tags = mapOf("a" to listOf(community.addressTag())), - kinds = listOf(CommunityPostApprovalEvent.KIND), + kinds = CommunityPostApprovalEvent.KIND_LIST, limit = 500, since = since, ), ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt index f7de6508a5..e820995bcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import kotlinx.coroutines.CoroutineScope // This allows multiple screen to be listening to tags, even the same tag @@ -36,8 +36,9 @@ class DiscoveryFilterAssembler( ) : ComposeSubscriptionManager() { val group = listOf( - DiscoveryFollowsDiscoverySubAssembler(client, ::allKeys), - MixGeohashHashtagsDiscoverySubAssembler(client, ::allKeys), + DiscoveryFollowsDiscoverySubAssembler1(client, ::allKeys), + DiscoveryFollowsDiscoverySubAssembler2(client, ::allKeys), + DiscoveryFollowsDiscoverySubAssembler3(client, ::allKeys), ) override fun start() = group.forEach { it.start() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt similarity index 62% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt index 6f6b41e812..52464b0512 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt @@ -20,37 +20,36 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByFollows -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByFollows -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByFollows -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByFollows -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByFollows -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByFollows -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByFollows -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.makeLongFormFilter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.makeContentDVMsFilter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.makeClassifiedsFilter import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch -import kotlin.collections.flatten -class DiscoveryFollowsDiscoverySubAssembler( +class DiscoveryFollowsDiscoverySubAssembler1( client: NostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: DiscoveryQueryState, - since: Map?, - ): List? = updateFilter(key.followsPerRelay(), key.followLists(), since) + since: SincePerRelayMap?, + ): List? { + val feedSettings = key.followsPerRelay() + + return makeLongFormFilter(feedSettings, since) + + makeClassifiedsFilter(feedSettings, since) + + makeContentDVMsFilter(feedSettings, since) + } override fun user(key: DiscoveryQueryState) = key.account.userProfile() @@ -60,32 +59,10 @@ class DiscoveryFollowsDiscoverySubAssembler( fun DiscoveryQueryState.listName() = listNameFlow().value - fun DiscoveryQueryState.followListsFlow() = account.liveDiscoveryFollowLists - - fun DiscoveryQueryState.followLists() = followListsFlow().value - - fun DiscoveryQueryState.followsPerRelayFlow() = account.liveDiscoveryListAuthorsPerRelay + fun DiscoveryQueryState.followsPerRelayFlow() = account.liveDiscoveryFollowListsPerRelay fun DiscoveryQueryState.followsPerRelay() = followsPerRelayFlow().value - fun updateFilter( - follows: Map>?, - followLists: Account.LiveFollowList?, - since: Map?, - ): List? { - if (follows != null && follows.isEmpty()) return null - - return listOfNotNull( - filterClassifiedsByFollows(follows, since), - filterFollowSetsByFollows(follows, since), - filterLongFormByFollows(follows, since), - filterPublicChatsByFollows(follows, since), - filterContentDVMsByFollows(follows, since), - filterLiveActivitiesByFollows(follows, followLists?.authorsPlusMe, since), - filterCommunitiesByFollows(follows, since), - ).flatten() - } - val userJobMap = mutableMapOf>() @OptIn(FlowPreview::class) @@ -113,7 +90,7 @@ class DiscoveryFollowsDiscoverySubAssembler( key: User, subId: String, ) { - return super.endSub(key, subId) + super.endSub(key, subId) userJobMap[key]?.forEach { it.cancel() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt new file mode 100644 index 0000000000..922385f2e0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.makeFollowSetsFilter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.makeLiveActivitiesFilter +import com.vitorpamplona.ammolite.relays.datasources.Subscription +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample +import kotlinx.coroutines.launch + +class DiscoveryFollowsDiscoverySubAssembler2( + client: NostrClient, + allKeys: () -> Set, +) : PerUserAndFollowListEoseManager(client, allKeys) { + override fun updateFilter( + key: DiscoveryQueryState, + since: SincePerRelayMap?, + ): List? { + val feedSettings = key.followsPerRelay() + + return makeFollowSetsFilter(feedSettings, since) + + makeLiveActivitiesFilter(feedSettings, since) + } + + override fun user(key: DiscoveryQueryState) = key.account.userProfile() + + override fun list(key: DiscoveryQueryState) = key.listName() + + fun DiscoveryQueryState.listNameFlow() = account.settings.defaultDiscoveryFollowList + + fun DiscoveryQueryState.listName() = listNameFlow().value + + fun DiscoveryQueryState.followsPerRelayFlow() = account.liveDiscoveryFollowListsPerRelay + + fun DiscoveryQueryState.followsPerRelay() = followsPerRelayFlow().value + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: DiscoveryQueryState): Subscription { + val user = user(key) + userJobMap[user]?.forEach { it.cancel() } + userJobMap[user] = + listOf( + key.scope.launch(Dispatchers.Default) { + key.listNameFlow().collectLatest { + invalidateFilters() + } + }, + key.scope.launch(Dispatchers.Default) { + key.followsPerRelayFlow().sample(5000).collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxUsersEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt similarity index 60% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxUsersEoseManager.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt index c2efb49270..0c9d240abc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxUsersEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt @@ -18,15 +18,16 @@ * 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.home.datasource.nip65Follows +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.makePublicChatsFilter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.makeCommunitiesFilter import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job @@ -34,31 +35,36 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch -class HomeOutboxUsersEoseManager( +class DiscoveryFollowsDiscoverySubAssembler3( client: NostrClient, - allKeys: () -> Set, -) : PerUserAndFollowListEoseManager(client, allKeys) { + allKeys: () -> Set, +) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( - key: HomeQueryState, - since: Map?, - ): List? = filterUserMetadataByFollows(key.followRelay(), since) + key: DiscoveryQueryState, + since: SincePerRelayMap?, + ): List? { + val feedSettings = key.followsPerRelay() - override fun user(key: HomeQueryState) = key.account.userProfile() + return makePublicChatsFilter(feedSettings, since) + + makeCommunitiesFilter(feedSettings, since) + } - override fun list(key: HomeQueryState) = key.listName() + override fun user(key: DiscoveryQueryState) = key.account.userProfile() - fun HomeQueryState.listNameFlow() = account.settings.defaultHomeFollowList + override fun list(key: DiscoveryQueryState) = key.listName() - fun HomeQueryState.listName() = listNameFlow().value + fun DiscoveryQueryState.listNameFlow() = account.settings.defaultDiscoveryFollowList - fun HomeQueryState.followRelayFlow() = account.liveHomeListAuthorsPerRelay + fun DiscoveryQueryState.listName() = listNameFlow().value - fun HomeQueryState.followRelay() = followRelayFlow().value + fun DiscoveryQueryState.followsPerRelayFlow() = account.liveDiscoveryFollowListsPerRelay + + fun DiscoveryQueryState.followsPerRelay() = followsPerRelayFlow().value val userJobMap = mutableMapOf>() @OptIn(FlowPreview::class) - override fun newSub(key: HomeQueryState): Subscription { + override fun newSub(key: DiscoveryQueryState): Subscription { val user = user(key) userJobMap[user]?.forEach { it.cancel() } userJobMap[user] = @@ -69,7 +75,7 @@ class HomeOutboxUsersEoseManager( } }, key.scope.launch(Dispatchers.Default) { - key.followRelayFlow().sample(5000).collectLatest { + key.followsPerRelayFlow().sample(5000).collectLatest { invalidateFilters() } }, @@ -82,7 +88,7 @@ class HomeOutboxUsersEoseManager( key: User, subId: String, ) { - return super.endSub(key, subId) + super.endSub(key, subId) userJobMap[key]?.forEach { it.cancel() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/MixGeohashHashtagsDiscoverySubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/MixGeohashHashtagsDiscoverySubAssembler.kt deleted file mode 100644 index 51660df20f..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/MixGeohashHashtagsDiscoverySubAssembler.kt +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource - -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByGeohash -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByHashtag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByGeohash -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByHashtag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByGeohash -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByHashtag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByGeohash -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByHashtag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunityPostsByGeohash -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunityPostsByHashtag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByGeohash -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByHashtag -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByGeohash -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByHashtag -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -class MixGeohashHashtagsDiscoverySubAssembler( - client: NostrClient, - allKeys: () -> Set, -) : PerUserEoseManager(client, allKeys) { - override fun updateFilter( - key: DiscoveryQueryState, - since: Map?, - ): List? = updateFilter(key.followLists()?.hashtags, key.followLists()?.geotags, since) - - fun updateFilter( - hashtags: Set?, - geotags: Set?, - since: Map?, - ): List? { - val hashtagFilters = - if (hashtags != null && !hashtags.isEmpty()) { - listOfNotNull( - filterClassifiedsByHashtag(hashtags, since), - filterPublicChatsByHashtag(hashtags, since), - filterFollowSetsByHashtag(hashtags, since), - filterLongFormByHashtag(hashtags, since), - filterContentDVMsByHashtag(hashtags, since), - filterLiveActivitiesByHashtag(hashtags, since), - filterCommunityPostsByHashtag(hashtags, since), - ).flatten() - } else { - emptyList() - } - - val geoHashFilters = - if (geotags != null && !geotags.isEmpty()) { - listOfNotNull( - filterClassifiedsByGeohash(geotags, since), - filterPublicChatsByGeohash(geotags, since), - filterFollowSetsByGeohash(geotags, since), - filterLongFormByGeohash(geotags, since), - filterContentDVMsByGeohash(geotags, since), - filterLiveActivitiesByGeohash(geotags, since), - filterCommunityPostsByGeohash(geotags, since), - ).flatten() - } else { - emptyList() - } - - return hashtagFilters + geoHashFilters - } - - override fun user(key: DiscoveryQueryState) = key.account.userProfile() - - fun DiscoveryQueryState.followListsFlow() = account.liveDiscoveryFollowLists - - fun DiscoveryQueryState.followLists() = followListsFlow().value - - val userJobMap = mutableMapOf() - - override fun newSub(key: DiscoveryQueryState): Subscription { - val user = user(key) - userJobMap[user]?.cancel() - userJobMap[user] = - key.scope.launch(Dispatchers.Default) { - key.followListsFlow().collectLatest { - invalidateFilters() - } - } - - return super.newSub(key) - } - - override fun endSub( - key: User, - subId: String, - ) { - return super.endSub(key, subId) - userJobMap[key]?.cancel() - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt index 868092d8b5..56140e4d4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt @@ -56,11 +56,9 @@ open class DiscoverLongFormFeedFilter( override fun applyFilter(collection: Set): Set = innerApplyFilter(collection) fun buildFilterParams(account: Account): FilterByListParams = - FilterByListParams.Companion.create( - account.userProfile().pubkeyHex, - account.settings.defaultDiscoveryFollowList.value, + FilterByListParams.create( account.liveDiscoveryFollowLists.value, - account.flowHiddenUsers.value, + account.hiddenUsers.flow.value, ) protected open fun innerApplyFilter(collection: Collection): Set { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt new file mode 100644 index 0000000000..9083c8e909 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByAllCommunities +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByAuthors +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByCommunity +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormGlobal +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter + +fun makeLongFormFilter( + feedSettings: IFeedTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + return when (feedSettings) { + is AllCommunitiesTopNavPerRelayFilterSet -> filterLongFormByAllCommunities(feedSettings, since) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterLongFormByFollows(feedSettings, since) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since) + is GlobalTopNavPerRelayFilterSet -> filterLongFormGlobal(feedSettings, since) + is HashtagTopNavPerRelayFilterSet -> filterLongFormByHashtag(feedSettings, since) + is LocationTopNavPerRelayFilterSet -> filterLongFormByGeohash(feedSettings, since) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since) + is SingleCommunityTopNavPerRelayFilterSet -> filterLongFormByCommunity(feedSettings, since) + else -> emptyList() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt new file mode 100644 index 0000000000..f08543c749 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterLongFormAllCommunities( + relay: NormalizedRelayUrl, + communities: Set, + since: Long? = null, +): List { + val communityList = communities.sorted() + + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to communityList, + "k" to listOf(LongTextNoteEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = mapOf("k" to listOf("5300"), "a" to communityList), + kinds = listOf(LongTextNoteEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterLongFormByAllCommunities( + communitySet: AllCommunitiesTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterLongFormAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt new file mode 100644 index 0000000000..838d31e009 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull + +fun filterLongFormAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + val authorList = authors.sorted() + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authorList, + kinds = listOf(LongTextNoteEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterLongFormByAuthors( + authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterLongFormAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} + +fun filterLongFormByAuthors( + authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterLongFormAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt new file mode 100644 index 0000000000..81570692bd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterLongFormByCommunity( + relay: NormalizedRelayUrl, + community: String, + authors: Set?, + since: Long? = null, +): List { + val authors = authors?.sorted() + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to listOf(community), + "k" to listOf(LongTextNoteEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + tags = mapOf("k" to listOf("5300"), "a" to listOf(community)), + kinds = listOf(LongTextNoteEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterLongFormByCommunity( + communitySet: SingleCommunityTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterLongFormByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt index e8eb237955..c3e1a7d47f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt @@ -20,29 +20,37 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatMap fun filterLongFormByFollows( - follows: Map>?, - since: Map?, -): List? { - if (follows != null && follows.isEmpty()) return null + followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (followsSet.set.isEmpty()) return emptyList() - return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(LongTextNoteEvent.KIND), - limit = 300, - since = since, - ), - ), - ) + val defaultSince = TimeUtils.oneWeekAgo() + + return followsSet.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + val relay = it.key + + listOfNotNull( + it.value.authors?.let { + filterLongFormAuthors(relay, it, since) + }, + it.value.geotags?.let { + filterLongFormByGeohash(relay, it, since) + }, + it.value.hashtags?.let { + filterLongFormByHashtag(relay, it, since) + }, + it.value.communities?.let { + filterLongFormAllCommunities(relay, it, since) + }, + ).flatten() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt index 88a4edd458..51f81e1379 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt @@ -20,25 +20,30 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByGeohash +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterLongFormByGeohash( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + geotags: Set, + since: Long? = null, +): List { + if (geotags.isEmpty()) return emptyList() - val geoHashes = hashToLoad.toList() + val geoHashes = geotags.sorted() return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( + Filter( kinds = listOf(LongTextNoteEvent.KIND), tags = mapOf("g" to geoHashes), limit = 300, @@ -47,3 +52,24 @@ fun filterLongFormByGeohash( ), ) } + +fun filterLongFormByGeohash( + geoSet: LocationTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (geoSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return geoSet.set.mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterLiveActivitiesByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt index b63040209d..71afa309c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt @@ -20,30 +20,31 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterLongFormByHashtag( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + hashtags: Set, + since: Long?, +): List { + if (hashtags.isEmpty()) return emptyList() - val hashtags = hashtagAlts(hashToLoad).toList() + val hashtags = hashtags.flatMap(::hashtagAlts).distinct().sorted() return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - LongTextNoteEvent.KIND, - ), + Filter( + kinds = listOf(LongTextNoteEvent.KIND), tags = mapOf("t" to hashtags), limit = 300, since = since, @@ -51,3 +52,24 @@ fun filterLongFormByHashtag( ), ) } + +fun filterLongFormByHashtag( + hashSet: HashtagTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (hashSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return hashSet.set.mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterLongFormByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt new file mode 100644 index 0000000000..c5445f7f21 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.utils.TimeUtils + +fun filterLongFormGlobal( + relays: GlobalTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (relays.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return relays.set.map { + val since = since?.get(it.key)?.time ?: defaultSince + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = listOf(LongTextNoteEvent.KIND), + limit = 300, + since = since, + ), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt index fdf74d7afe..f43b0e5a14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent @@ -46,16 +45,12 @@ open class DiscoverChatFeedFilter( val params = buildFilterParams(account) val allChannelNotes = - LocalCache.channels.mapNotNullIntoSet { _, channel -> - if (channel is PublicChatChannel) { - val note = LocalCache.getNoteIfExists(channel.idHex) - val noteEvent = note?.event + LocalCache.publicChatChannels.mapNotNullIntoSet { _, channel -> + val note = LocalCache.getNoteIfExists(channel.idHex) + val noteEvent = note?.event - if (noteEvent == null || params.match(noteEvent)) { - note - } else { - null - } + if (noteEvent == null || params.match(noteEvent)) { + note } else { null } @@ -67,11 +62,9 @@ open class DiscoverChatFeedFilter( override fun applyFilter(collection: Set): Set = innerApplyFilter(collection) fun buildFilterParams(account: Account): FilterByListParams = - FilterByListParams.Companion.create( - userHex = account.userProfile().pubkeyHex, - selectedListName = account.settings.defaultDiscoveryFollowList.value, + FilterByListParams.create( followLists = account.liveDiscoveryFollowLists.value, - hiddenUsers = account.flowHiddenUsers.value, + hiddenUsers = account.hiddenUsers.flow.value, ) protected open fun innerApplyFilter(collection: Collection): Set { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderChannelThumb.kt index f4f8ddbb70..70249b8dcd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderChannelThumb.kt @@ -45,6 +45,10 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.ui.navigation.INav @@ -106,9 +110,17 @@ fun RenderChannelThumb( LaunchedEffect(key1 = channelUpdates) { launch(Dispatchers.IO) { - val followingKeySet = - accountViewModel.account.liveDiscoveryFollowLists.value - ?.authors + val topFilter = accountViewModel.account.liveDiscoveryFollowLists.value + val topFilterAuthors = + when (topFilter) { + is AuthorsByOutboxTopNavFilter -> topFilter.authors + is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors + is AllFollowsByOutboxTopNavFilter -> topFilter.authors + is SingleCommunityTopNavFilter -> topFilter.authors + else -> null + } + + val followingKeySet = topFilterAuthors val allParticipants = ParticipantListBuilder() .followsThatParticipateOn(baseNote, followingKeySet) @@ -116,7 +128,7 @@ fun RenderChannelThumb( val newParticipantUsers = if (followingKeySet == null) { - val allFollows = accountViewModel.account.liveKind3Follows.value.authors + val allFollows = accountViewModel.account.kind3FollowList.flow.value.authors val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).toList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt new file mode 100644 index 0000000000..d3db8579c1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByAllCommunities +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByAuthors +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByCommunity +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsGlobal +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter + +fun makePublicChatsFilter( + feedSettings: IFeedTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + return when (feedSettings) { + is AllCommunitiesTopNavPerRelayFilterSet -> filterPublicChatsByAllCommunities(feedSettings, since) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByFollows(feedSettings, since) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since) + is GlobalTopNavPerRelayFilterSet -> filterPublicChatsGlobal(feedSettings, since) + is HashtagTopNavPerRelayFilterSet -> filterPublicChatsByHashtag(feedSettings, since) + is LocationTopNavPerRelayFilterSet -> filterPublicChatsByGeohash(feedSettings, since) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since) + is SingleCommunityTopNavPerRelayFilterSet -> filterPublicChatsByCommunity(feedSettings, since) + else -> emptyList() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt new file mode 100644 index 0000000000..130409fa62 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterPublicChatsAllCommunities( + relay: NormalizedRelayUrl, + communities: Set, + since: Long? = null, +): List { + val communityList = communities.sorted() + + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to communityList, + "k" to listOf(ChannelCreateEvent.KIND.toString(), ChannelMetadataEvent.KIND.toString(), ChannelMessageEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = mapOf("k" to listOf("5300"), "a" to communityList), + kinds = + listOf( + ChannelCreateEvent.KIND, + ChannelMetadataEvent.KIND, + ChannelMessageEvent.KIND, + ), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterPublicChatsByAllCommunities( + communitySet: AllCommunitiesTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterPublicChatsAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt new file mode 100644 index 0000000000..3aedb18754 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull + +fun filterPublicChatsAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + val authorList = authors.sorted() + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authorList, + kinds = + listOf( + ChannelCreateEvent.KIND, + ChannelMetadataEvent.KIND, + ChannelMessageEvent.KIND, + ), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterPublicChatsByAuthors( + authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterPublicChatsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} + +fun filterPublicChatsByAuthors( + authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterPublicChatsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt new file mode 100644 index 0000000000..ce0975292b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterPublicChatsByCommunity( + relay: NormalizedRelayUrl, + community: String, + authors: Set?, + since: Long? = null, +): List { + val authors = authors?.sorted() + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to listOf(community), + "k" to listOf(ChannelCreateEvent.KIND.toString(), ChannelMetadataEvent.KIND.toString(), ChannelMessageEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + tags = mapOf("k" to listOf("5300"), "a" to listOf(community)), + kinds = + listOf( + ChannelCreateEvent.KIND, + ChannelMetadataEvent.KIND, + ChannelMessageEvent.KIND, + ), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterPublicChatsByCommunity( + communitySet: SingleCommunityTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterPublicChatsByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt index 597fe822bc..1c57ba3741 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt @@ -20,29 +20,37 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatMap fun filterPublicChatsByFollows( - follows: Map>?, - since: Map?, -): List? { - if (follows != null && follows.isEmpty()) return null + followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (followsSet.set.isEmpty()) return emptyList() - return listOf( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(ChannelMessageEvent.KIND), - limit = 500, - since = since, - ), - ), - ) + val defaultSince = TimeUtils.oneWeekAgo() + + return followsSet.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + val relay = it.key + + listOfNotNull( + it.value.authors?.let { + filterPublicChatsAuthors(relay, it, since) + }, + it.value.geotags?.let { + filterPublicChatsByGeohash(relay, it, since) + }, + it.value.hashtags?.let { + filterPublicChatsByHashtag(relay, it, since) + }, + it.value.communities?.let { + filterPublicChatsAllCommunities(relay, it, since) + }, + ).flatten() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt index ecd45458c0..fafc069a5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt @@ -20,27 +20,31 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterPublicChatsByGeohash( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + geotags: Set, + since: Long? = null, +): List { + if (geotags.isEmpty()) return emptyList() - val geoHashes = hashToLoad.toList() + val geoHashes = geotags.sorted() return listOf( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( + Filter( kinds = listOf( ChannelCreateEvent.KIND, @@ -54,3 +58,24 @@ fun filterPublicChatsByGeohash( ), ) } + +fun filterPublicChatsByGeohash( + geoSet: LocationTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (geoSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return geoSet.set.mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterPublicChatsByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt index 7a4570c223..d7a69d40bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt @@ -20,28 +20,32 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterPublicChatsByHashtag( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + hashtags: Set, + since: Long?, +): List { + if (hashtags.isEmpty()) return emptyList() - val hashtags = hashtagAlts(hashToLoad).toList() + val hashtags = hashtags.flatMap(::hashtagAlts).distinct().sorted() return listOf( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( + Filter( kinds = listOf( ChannelCreateEvent.KIND, @@ -55,3 +59,24 @@ fun filterPublicChatsByHashtag( ), ) } + +fun filterPublicChatsByHashtag( + hashSet: HashtagTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (hashSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return hashSet.set.mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterPublicChatsByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt similarity index 54% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByHashtag.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt index 72fdf25b64..3b4062471f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt @@ -18,38 +18,40 @@ * 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.discover.nip72Communities.subassemblies +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts -import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.utils.TimeUtils -fun filterCommunityPostsByHashtag( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null +fun filterPublicChatsGlobal( + relays: GlobalTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (relays.set.isEmpty()) return emptyList() - val hashtags = hashtagAlts(hashToLoad).toList() + val defaultSince = TimeUtils.oneWeekAgo() - return listOf( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), + return relays.set.map { + val since = since?.get(it.key)?.time ?: defaultSince + RelayBasedFilter( + relay = it.key, filter = - SincePerRelayFilter( + Filter( kinds = listOf( - CommunityDefinitionEvent.KIND, - CommunityPostApprovalEvent.KIND, + ChannelCreateEvent.KIND, + ChannelMetadataEvent.KIND, + ChannelMessageEvent.KIND, ), - tags = mapOf("t" to hashtags), limit = 300, since = since, ), - ), - ) + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt index 8f0dd3b7b3..d1a9c0d809 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt @@ -56,11 +56,9 @@ open class DiscoverFollowSetsFeedFilter( override fun applyFilter(collection: Set): Set = innerApplyFilter(collection) fun buildFilterParams(account: Account): FilterByListParams = - FilterByListParams.Companion.create( - account.userProfile().pubkeyHex, - account.settings.defaultDiscoveryFollowList.value, + FilterByListParams.create( account.liveDiscoveryFollowLists.value, - account.flowHiddenUsers.value, + account.hiddenUsers.flow.value, ) protected open fun innerApplyFilter(collection: Collection): Set { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt new file mode 100644 index 0000000000..bb5a3f60a3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByAllCommunities +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByAuthors +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByCommunity +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsGlobal +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter + +fun makeFollowSetsFilter( + feedSettings: IFeedTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + return when (feedSettings) { + is AllCommunitiesTopNavPerRelayFilterSet -> filterFollowSetsByAllCommunities(feedSettings, since) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByFollows(feedSettings, since) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since) + is GlobalTopNavPerRelayFilterSet -> filterFollowSetsGlobal(feedSettings, since) + is HashtagTopNavPerRelayFilterSet -> filterFollowSetsByHashtag(feedSettings, since) + is LocationTopNavPerRelayFilterSet -> filterFollowSetsByGeohash(feedSettings, since) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since) + is SingleCommunityTopNavPerRelayFilterSet -> filterFollowSetsByCommunity(feedSettings, since) + else -> emptyList() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt new file mode 100644 index 0000000000..21ec68b6cf --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsAllCommunities +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterFollowSetsAllCommunities( + relay: NormalizedRelayUrl, + communities: Set, + since: Long? = null, +): List { + val communityList = communities.sorted() + + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to communityList, + "k" to listOf(FollowListEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = mapOf("k" to listOf("5300"), "a" to communityList), + kinds = listOf(FollowListEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterFollowSetsByAllCommunities( + communitySet: AllCommunitiesTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterContentDVMsAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt new file mode 100644 index 0000000000..d37e3e3920 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull + +fun filterFollowSetsAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + val authorList = authors.sorted() + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authorList, + kinds = listOf(FollowListEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterFollowSetsByAuthors( + authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterFollowSetsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} + +fun filterFollowSetsByAuthors( + authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterFollowSetsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt new file mode 100644 index 0000000000..7a0fb3f66b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterFollowSetsByCommunity( + relay: NormalizedRelayUrl, + community: String, + authors: Set?, + since: Long? = null, +): List { + val authors = authors?.sorted() + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to listOf(community), + "k" to listOf(FollowListEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + tags = mapOf("k" to listOf("5300"), "a" to listOf(community)), + kinds = listOf(FollowListEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterFollowSetsByCommunity( + communitySet: SingleCommunityTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterFollowSetsByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt index 129ae8bf34..60acbeabba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt @@ -20,29 +20,37 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatMap fun filterFollowSetsByFollows( - follows: Map>?, - since: Map?, -): List? { - if (follows != null && follows.isEmpty()) return null + followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (followsSet.set.isEmpty()) return emptyList() - return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(FollowListEvent.KIND), - limit = 300, - since = since, - ), - ), - ) + val defaultSince = TimeUtils.oneWeekAgo() + + return followsSet.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + val relay = it.key + + listOfNotNull( + it.value.authors?.let { + filterFollowSetsAuthors(relay, it, since) + }, + it.value.geotags?.let { + filterFollowSetsByGeohash(relay, it, since) + }, + it.value.hashtags?.let { + filterFollowSetsByHashtag(relay, it, since) + }, + it.value.communities?.let { + filterFollowSetsAllCommunities(relay, it, since) + }, + ).flatten() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt index dfe845e0cd..1ab2287ab9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt @@ -20,26 +20,30 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterFollowSetsByGeohash( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + geotags: Set, + since: Long? = null, +): List { + if (geotags.isEmpty()) return emptyList() - val geoHashes = hashToLoad.toList() + val geoHashes = geotags.sorted() return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = listOf(LongTextNoteEvent.KIND), + Filter( + kinds = listOf(FollowListEvent.KIND), tags = mapOf("g" to geoHashes), limit = 300, since = since, @@ -47,3 +51,24 @@ fun filterFollowSetsByGeohash( ), ) } + +fun filterFollowSetsByGeohash( + geoSet: LocationTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (geoSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return geoSet.set.mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterFollowSetsByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt index 7fc277b392..530015ce04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt @@ -20,30 +20,31 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterFollowSetsByHashtag( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + hashtags: Set, + since: Long?, +): List { + if (hashtags.isEmpty()) return emptyList() - val hashtags = hashtagAlts(hashToLoad).toList() + val hashtags = hashtags.flatMap(::hashtagAlts).distinct().sorted() return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - LongTextNoteEvent.KIND, - ), + Filter( + kinds = listOf(FollowListEvent.KIND), tags = mapOf("t" to hashtags), limit = 300, since = since, @@ -51,3 +52,24 @@ fun filterFollowSetsByHashtag( ), ) } + +fun filterFollowSetsByHashtag( + hashSet: HashtagTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (hashSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return hashSet.set.mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterFollowSetsByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt similarity index 54% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt index 8770b335b5..ebc2943481 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterQuotesToNotes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt @@ -18,32 +18,33 @@ * 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.service.relayClient.reqCommand.event.watchers +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.ammolite.relays.EVENT_FINDER_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.utils.TimeUtils -fun filterQuotesToNotes( - notes: List, - since: Map?, -): List? { - if (notes.isEmpty()) return null +fun filterFollowSetsGlobal( + relays: GlobalTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (relays.set.isEmpty()) return emptyList() - return listOf( - TypedFilter( - types = EVENT_FINDER_TYPES, + val defaultSince = TimeUtils.oneWeekAgo() + + return relays.set.map { + val since = since?.get(it.key)?.time ?: defaultSince + RelayBasedFilter( + relay = it.key, filter = - SincePerRelayFilter( - kinds = listOf(TextNoteEvent.KIND), - tags = mapOf("q" to notes.map { it.idHex }.sorted()), + Filter( + kinds = listOf(FollowListEvent.KIND), + limit = 300, since = since, - // Max amount of "replies" to download on a specific event. - limit = 1000, ), - ), - ) + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt index 3437801917..09660985be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt @@ -24,6 +24,10 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip51Lists.MuteListEvent @@ -43,8 +47,8 @@ open class DiscoverLiveFeedFilter( followList() == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) override fun feed(): List { - val allChannelNotes = LocalCache.channels.mapNotNull { _, channel -> LocalCache.getNoteIfExists(channel.idHex) } - val allMessageNotes = LocalCache.channels.map { _, channel -> channel.notes.filter { key, it -> it.event is LiveActivitiesEvent } }.flatten() + val allChannelNotes = LocalCache.liveChatChannels.mapNotNull { _, channel -> LocalCache.getNoteIfExists(channel.idHex) } + val allMessageNotes = LocalCache.liveChatChannels.map { _, channel -> channel.notes.filter { key, it -> it.event is LiveActivitiesEvent } }.flatten() val notes = innerApplyFilter(allChannelNotes + allMessageNotes) @@ -55,11 +59,9 @@ open class DiscoverLiveFeedFilter( protected open fun innerApplyFilter(collection: Collection): Set { val filterParams = - FilterByListParams.Companion.create( - userHex = account.userProfile().pubkeyHex, - selectedListName = account.settings.defaultDiscoveryFollowList.value, + FilterByListParams.create( followLists = account.liveDiscoveryFollowLists.value, - hiddenUsers = account.flowHiddenUsers.value, + hiddenUsers = account.hiddenUsers.flow.value, ) return collection.filterTo(HashSet()) { @@ -69,8 +71,18 @@ open class DiscoverLiveFeedFilter( } override fun sort(collection: Set): List { + val topFilter = account.liveDiscoveryFollowLists.value + val discoveryTopFilterAuthors = + when (topFilter) { + is AuthorsByOutboxTopNavFilter -> topFilter.authors + is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors + is AllFollowsByOutboxTopNavFilter -> topFilter.authors + is SingleCommunityTopNavFilter -> topFilter.authors + else -> null + } + val followingKeySet = - account.liveDiscoveryFollowLists.value?.authors ?: account.liveKind3Follows.value.authors + discoveryTopFilterAuthors ?: account.kind3FollowList.flow.value.authors val counter = ParticipantListBuilder() val participantCounts = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 41dfa2f864..775ca02efb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -45,6 +45,10 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.navigation.INav @@ -222,9 +226,16 @@ fun LoadParticipants( val hostsAuthor = hosts + (baseNote.author?.let { listOf(it) } ?: emptyList()) + val topFilter = accountViewModel.account.liveDiscoveryFollowLists.value + val followingKeySet = - accountViewModel.account.liveDiscoveryFollowLists.value - ?.authors + when (topFilter) { + is AuthorsByOutboxTopNavFilter -> topFilter.authors + is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors + is AllFollowsByOutboxTopNavFilter -> topFilter.authors + is SingleCommunityTopNavFilter -> topFilter.authors + else -> emptySet() + } val allParticipants = ParticipantListBuilder() @@ -233,7 +244,7 @@ fun LoadParticipants( val newParticipantUsers = if (followingKeySet == null) { - val allFollows = accountViewModel.account.liveKind3Follows.value.authors + val allFollows = accountViewModel.account.kind3FollowList.flow.value.authors val followingParticipants = ParticipantListBuilder() .followsThatParticipateOn(baseNote, allFollows) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt new file mode 100644 index 0000000000..13acbd03ee --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByAllCommunities +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByAuthors +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByCommunity +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesGlobal +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter + +fun makeLiveActivitiesFilter( + feedSettings: IFeedTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + return when (feedSettings) { + is AllCommunitiesTopNavPerRelayFilterSet -> filterLiveActivitiesByAllCommunities(feedSettings, since) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByFollows(feedSettings, since) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since) + is GlobalTopNavPerRelayFilterSet -> filterLiveActivitiesGlobal(feedSettings, since) + is HashtagTopNavPerRelayFilterSet -> filterLiveActivitiesByHashtag(feedSettings, since) + is LocationTopNavPerRelayFilterSet -> filterLiveActivitiesByGeohash(feedSettings, since) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since) + is SingleCommunityTopNavPerRelayFilterSet -> filterLiveActivitiesByCommunity(feedSettings, since) + else -> emptyList() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt new file mode 100644 index 0000000000..c139cea3d7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsAllCommunities +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterLiveActivitiesAllCommunities( + relay: NormalizedRelayUrl, + communities: Set, + since: Long? = null, +): List { + val communityList = communities.sorted() + + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to communityList, + "k" to listOf(LiveActivitiesChatMessageEvent.KIND.toString(), LiveActivitiesEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = mapOf("k" to listOf("5300"), "a" to communityList), + kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterLiveActivitiesByAllCommunities( + communitySet: AllCommunitiesTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterContentDVMsAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt new file mode 100644 index 0000000000..84225a667e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull + +fun filterLiveActivitiesAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + val authorList = authors.sorted() + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authorList, + kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), + limit = 300, + since = since, + ), + ), + // authors are participating in the live event. + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = mapOf("p" to authorList), + kinds = listOf(LiveActivitiesEvent.KIND), + limit = 100, + since = since, + ), + ), + ) +} + +fun filterLiveActivitiesByAuthors( + authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterLiveActivitiesAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} + +fun filterLiveActivitiesByAuthors( + authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterLiveActivitiesAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt new file mode 100644 index 0000000000..382c0d6332 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterLiveActivitiesByCommunity( + relay: NormalizedRelayUrl, + community: String, + authors: Set?, + since: Long? = null, +): List { + val authors = authors?.sorted() + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to listOf(community), + "k" to listOf(LiveActivitiesChatMessageEvent.KIND.toString(), LiveActivitiesEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + tags = mapOf("k" to listOf("5300"), "a" to listOf(community)), + kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterLiveActivitiesByCommunity( + communitySet: SingleCommunityTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterLiveActivitiesByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt index f69c59490b..40ee82d9b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt @@ -20,48 +20,37 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatMap fun filterLiveActivitiesByFollows( - follows: Map>?, - followKeys: Set?, - since: Map?, -): List? { - if (follows != null && follows.isEmpty()) return null + followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (followsSet.set.isEmpty()) return emptyList() - return listOfNotNull( - TypedFilter( - types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = - listOf( - LiveActivitiesChatMessageEvent.KIND, - LiveActivitiesEvent.KIND, - ), - limit = 300, - since = since, - ), - ), - followKeys?.let { - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SincePerRelayFilter( - tags = mapOf("p" to it.toList()), - kinds = listOf(LiveActivitiesEvent.KIND), - limit = 100, - since = since, - ), - ) - }, - ) + val defaultSince = TimeUtils.oneWeekAgo() + + return followsSet.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + val relay = it.key + + listOfNotNull( + it.value.authors?.let { + filterLiveActivitiesAuthors(relay, it, since) + }, + it.value.geotags?.let { + filterLiveActivitiesByGeohash(relay, it, since) + }, + it.value.hashtags?.let { + filterLiveActivitiesByHashtag(relay, it, since) + }, + it.value.communities?.let { + filterLiveActivitiesAllCommunities(relay, it, since) + }, + ).flatten() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt index 8ad8955436..8a9470a0c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt @@ -20,31 +20,31 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterLiveActivitiesByGeohash( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + geotags: Set, + since: Long? = null, +): List { + if (geotags.isEmpty()) return emptyList() - val geoHashes = hashToLoad.toList() + val geoHashes = geotags.sorted() return listOf( - TypedFilter( - types = setOf(FeedType.GLOBAL), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - LiveActivitiesChatMessageEvent.KIND, - LiveActivitiesEvent.KIND, - ), + Filter( + kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), tags = mapOf("g" to geoHashes), limit = 300, since = since, @@ -52,3 +52,24 @@ fun filterLiveActivitiesByGeohash( ), ) } + +fun filterLiveActivitiesByGeohash( + geoSet: LocationTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (geoSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return geoSet.set.mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterLiveActivitiesByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt index 3b67b868c3..32bd409ca1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt @@ -20,32 +20,32 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterLiveActivitiesByHashtag( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + hashtags: Set, + since: Long?, +): List { + if (hashtags.isEmpty()) return emptyList() - val hashtags = hashtagAlts(hashToLoad).toList() + val hashtags = hashtags.flatMap(::hashtagAlts).distinct().sorted() return listOf( - TypedFilter( - types = setOf(FeedType.GLOBAL), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - LiveActivitiesChatMessageEvent.KIND, - LiveActivitiesEvent.KIND, - ), + Filter( + kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), tags = mapOf("t" to hashtags), limit = 300, since = since, @@ -53,3 +53,24 @@ fun filterLiveActivitiesByHashtag( ), ) } + +fun filterLiveActivitiesByHashtag( + hashSet: HashtagTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (hashSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return hashSet.set.mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterLiveActivitiesByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterUserMetadataByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt similarity index 50% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterUserMetadataByFollows.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt index b9f3b5c3ab..5b9c7edd54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterUserMetadataByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt @@ -18,35 +18,34 @@ * 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.home.datasource.nip65Follows +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.utils.TimeUtils -fun filterUserMetadataByFollows( - follows: Map>?, - since: Map?, -): List { - if (follows != null && follows.isEmpty()) return emptyList() +fun filterLiveActivitiesGlobal( + relays: GlobalTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (relays.set.isEmpty()) return emptyList() - return listOf( - TypedFilter( - types = setOf(if (follows != null) FeedType.FOLLOWS else FeedType.GLOBAL), + val defaultSince = TimeUtils.oneWeekAgo() + + return relays.set.map { + val since = since?.get(it.key)?.time ?: defaultSince + RelayBasedFilter( + relay = it.key, filter = - SinceAuthorPerRelayFilter( - kinds = - listOf( - MetadataEvent.KIND, - AdvertisedRelayListEvent.KIND, - ), - authors = follows, + Filter( + kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), + limit = 300, since = since, ), - ), - ) + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt index 460f8d3ba6..464b935122 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt @@ -47,6 +47,10 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.ui.navigation.INav @@ -197,15 +201,23 @@ fun LoadModerators( } } - val followingKeySet = - accountViewModel.account.liveDiscoveryFollowLists.value - ?.authors + val topFilter = accountViewModel.account.liveDiscoveryFollowLists.value + val discoveryTopFilterAuthors = + when (topFilter) { + is AuthorsByOutboxTopNavFilter -> topFilter.authors + is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors + is AllFollowsByOutboxTopNavFilter -> topFilter.authors + is SingleCommunityTopNavFilter -> topFilter.authors + else -> null + } + + val followingKeySet = discoveryTopFilterAuthors val allParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, followingKeySet).minus(hosts) val newParticipantUsers = if (followingKeySet == null) { - val allFollows = accountViewModel.account.liveKind3Follows.value.authors + val allFollows = accountViewModel.account.kind3FollowList.flow.value.authors val followingParticipants = ParticipantListBuilder().followsThatParticipateOn(baseNote, allFollows).minus(hosts) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt index c4b3ad02e5..0ee01359de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent @@ -44,18 +45,16 @@ open class DiscoverCommunityFeedFilter( override fun feed(): List { val filterParams = - FilterByListParams.Companion.create( - userHex = account.userProfile().pubkeyHex, - selectedListName = account.settings.defaultDiscoveryFollowList.value, + FilterByListParams.create( followLists = account.liveDiscoveryFollowLists.value, - hiddenUsers = account.flowHiddenUsers.value, + hiddenUsers = account.hiddenUsers.flow.value, ) // Here we only need to look for CommunityDefinition Events val notes = LocalCache.addressables.mapNotNullIntoSet { key, note -> val noteEvent = note.event - if (noteEvent == null && shouldInclude(Address.Companion.parse(key), filterParams)) { + if (noteEvent == null && shouldInclude(key, filterParams, note.relays)) { // send unloaded communities to the screen note } else if (noteEvent is CommunityDefinitionEvent && filterParams.match(noteEvent)) { @@ -73,11 +72,9 @@ open class DiscoverCommunityFeedFilter( protected open fun innerApplyFilter(collection: Collection): Set { // here, we need to look for CommunityDefinition in new collection AND new CommunityDefinition from Post Approvals val filterParams = - FilterByListParams.Companion.create( - userHex = account.userProfile().pubkeyHex, - selectedListName = account.settings.defaultDiscoveryFollowList.value, + FilterByListParams.create( followLists = account.liveDiscoveryFollowLists.value, - hiddenUsers = account.flowHiddenUsers.value, + hiddenUsers = account.hiddenUsers.flow.value, ) return collection @@ -91,7 +88,7 @@ open class DiscoverCommunityFeedFilter( val definitionNote = LocalCache.getOrCreateAddressableNote(it) val definitionEvent = definitionNote.event - if (definitionEvent == null && shouldInclude(it, filterParams)) { + if (definitionEvent == null && shouldInclude(it, filterParams, definitionNote.relays)) { definitionNote } else if (definitionEvent is CommunityDefinitionEvent && filterParams.match(definitionEvent)) { definitionNote @@ -109,7 +106,8 @@ open class DiscoverCommunityFeedFilter( private fun shouldInclude( aTag: Address?, params: FilterByListParams, - ) = aTag != null && aTag.kind == CommunityDefinitionEvent.KIND && params.match(aTag) + comingFrom: List = emptyList(), + ) = aTag != null && aTag.kind == CommunityDefinitionEvent.KIND && params.match(aTag, comingFrom) override fun sort(collection: Set): List { val lastNote = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt new file mode 100644 index 0000000000..97576f15ef --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByAllCommunities +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByAuthors +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByCommunity +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesGlobal +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter + +fun makeCommunitiesFilter( + feedSettings: IFeedTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + return when (feedSettings) { + is AllCommunitiesTopNavPerRelayFilterSet -> filterCommunitiesByAllCommunities(feedSettings, since) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByFollows(feedSettings, since) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since) + is GlobalTopNavPerRelayFilterSet -> filterCommunitiesGlobal(feedSettings, since) + is HashtagTopNavPerRelayFilterSet -> filterCommunitiesByHashtag(feedSettings, since) + is LocationTopNavPerRelayFilterSet -> filterCommunitiesByGeohash(feedSettings, since) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since) + is SingleCommunityTopNavPerRelayFilterSet -> filterCommunitiesByCommunity(feedSettings, since) + else -> emptyList() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt new file mode 100644 index 0000000000..3dff4e38ca --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterCommunitiesAllCommunities( + relay: NormalizedRelayUrl, + communities: Set, + since: Long? = null, +): List { + val communityList = communities.sorted() + + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = CommunityPostApprovalEvent.KIND_LIST, + ids = communityList, + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = mapOf("a" to communityList), + kinds = listOf(ClassifiedsEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterCommunitiesByAllCommunities( + communitySet: AllCommunitiesTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterCommunitiesAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt new file mode 100644 index 0000000000..b1dab2df3e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull + +fun filterCommunitiesAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + val authorList = authors.sorted() + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authorList, + kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterCommunitiesByAuthors( + authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterCommunitiesAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} + +fun filterCommunitiesByAuthors( + authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterCommunitiesAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt new file mode 100644 index 0000000000..4a1bc7b7e0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterClassifiedsByCommunity( + relay: NormalizedRelayUrl, + community: String, + authors: Set?, + since: Long? = null, +): List { + val authors = authors?.sorted() + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + kinds = listOf(CommunityDefinitionEvent.KIND), + ids = listOf(community), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterCommunitiesByCommunity( + communitySet: SingleCommunityTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterClassifiedsByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt index 54bfbb8b90..014ac2d36f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt @@ -20,34 +20,37 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatMap fun filterCommunitiesByFollows( - follows: Map>?, - since: Map?, -): List? { - if (follows != null && follows.isEmpty()) return null + followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (followsSet.set.isEmpty()) return emptyList() - return listOf( - TypedFilter( - types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = - listOf( - CommunityDefinitionEvent.KIND, - CommunityPostApprovalEvent.KIND, - ), - limit = 300, - since = since, - ), - ), - ) + val defaultSince = TimeUtils.oneWeekAgo() + + return followsSet.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + val relay = it.key + + listOfNotNull( + it.value.authors?.let { + filterCommunitiesAuthors(relay, it, since) + }, + it.value.geotags?.let { + filterCommunitiesByGeohash(relay, it, since) + }, + it.value.hashtags?.let { + filterCommunitiesByHashtag(relay, it, since) + }, + it.value.communities?.let { + filterCommunitiesAllCommunities(relay, it, since) + }, + ).flatten() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt new file mode 100644 index 0000000000..338fb283f8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull + +fun filterCommunitiesByGeohash( + relay: NormalizedRelayUrl, + geotags: Set, + since: Long? = null, +): List { + if (geotags.isEmpty()) return emptyList() + + val geoHashes = geotags.sorted() + + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), + tags = mapOf("g" to geoHashes), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterCommunitiesByGeohash( + geoSet: LocationTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (geoSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return geoSet.set.mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterCommunitiesByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt new file mode 100644 index 0000000000..0a6aa4afdb --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull + +fun filterCommunitiesByHashtag( + relay: NormalizedRelayUrl, + hashtags: Set?, + since: Long?, +): List { + if (hashtags == null || hashtags.isEmpty()) return emptyList() + + val hashtags = hashtags.flatMap(::hashtagAlts).distinct().sorted() + + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), + tags = mapOf("t" to hashtags), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterCommunitiesByHashtag( + hashSet: HashtagTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (hashSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return hashSet.set.mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterCommunitiesByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt similarity index 57% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByGeohash.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt index 11dad756f8..68e42b47a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunityPostsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt @@ -20,35 +20,34 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils -fun filterCommunityPostsByGeohash( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null +fun filterCommunitiesGlobal( + relays: GlobalTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (relays.set.isEmpty()) return emptyList() - val geoHashes = hashToLoad.toList() + val defaultSince = TimeUtils.oneWeekAgo() - return listOf( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), - filter = - SincePerRelayFilter( - kinds = - listOf( - CommunityDefinitionEvent.KIND, - CommunityPostApprovalEvent.KIND, - ), - tags = mapOf("g" to geoHashes), - limit = 300, - since = since, - ), - ), - ) + return relays.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + listOf( + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), + limit = 300, + since = since, + ), + ), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt index d4aaa581b0..f790a49ede 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt @@ -56,11 +56,9 @@ open class DiscoverNIP89FeedFilter( override fun applyFilter(collection: Set): Set = innerApplyFilter(collection) fun buildFilterParams(account: Account): FilterByListParams = - FilterByListParams.Companion.create( - account.userProfile().pubkeyHex, - account.settings.defaultDiscoveryFollowList.value, + FilterByListParams.create( account.liveDiscoveryFollowLists.value, - account.flowHiddenUsers.value, + account.hiddenUsers.flow.value, ) fun acceptDVM(note: Note): Boolean { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt new file mode 100644 index 0000000000..3438e6ef22 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByAllCommunities +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByAuthors +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByCommunity +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsGlobal +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter + +fun makeContentDVMsFilter( + feedSettings: IFeedTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + return when (feedSettings) { + is AllCommunitiesTopNavPerRelayFilterSet -> filterContentDVMsByAllCommunities(feedSettings, since) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByFollows(feedSettings, since) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since) + is GlobalTopNavPerRelayFilterSet -> filterContentDVMsGlobal(feedSettings, since) + is HashtagTopNavPerRelayFilterSet -> filterContentDVMsByHashtag(feedSettings, since) + is LocationTopNavPerRelayFilterSet -> filterContentDVMsByGeohash(feedSettings, since) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since) + is SingleCommunityTopNavPerRelayFilterSet -> filterContentDVMsByCommunity(feedSettings, since) + else -> emptyList() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt new file mode 100644 index 0000000000..04b0963091 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterContentDVMsAllCommunities( + relay: NormalizedRelayUrl, + communities: Set, + since: Long? = null, +): List { + val communityList = communities.sorted() + + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to communityList, + "k" to listOf(AppDefinitionEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = mapOf("k" to listOf("5300"), "a" to communityList), + kinds = listOf(AppDefinitionEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterContentDVMsByAllCommunities( + communitySet: AllCommunitiesTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterContentDVMsAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt new file mode 100644 index 0000000000..2c7cfa0ae6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull + +fun filterContentDVMsAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + val authorList = authors.sorted() + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authorList, + kinds = listOf(AppDefinitionEvent.KIND), + tags = mapOf("k" to listOf("5300")), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterContentDVMsByAuthors( + authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterContentDVMsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} + +fun filterContentDVMsByAuthors( + authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterContentDVMsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt new file mode 100644 index 0000000000..8c0e586d1e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterContentDVMsByCommunity( + relay: NormalizedRelayUrl, + community: String, + authors: Set?, + since: Long? = null, +): List { + val authors = authors?.sorted() + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to listOf(community), + "k" to listOf(AppDefinitionEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + tags = mapOf("k" to listOf("5300"), "a" to listOf(community)), + kinds = listOf(AppDefinitionEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterContentDVMsByCommunity( + communitySet: SingleCommunityTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterContentDVMsByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt index 83dce49934..6cf4306250 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt @@ -20,29 +20,37 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatMap fun filterContentDVMsByFollows( - follows: Map>?, - since: Map?, -): List? { - if (follows != null && follows.isEmpty()) return null + followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (followsSet.set.isEmpty()) return emptyList() - return listOf( - TypedFilter( - types = if (follows == null) setOf(FeedType.GLOBAL) else setOf(FeedType.FOLLOWS), - filter = - SincePerRelayFilter( - kinds = listOf(AppDefinitionEvent.KIND), - limit = 300, - tags = mapOf("k" to listOf("5300")), - since = since, - ), - ), - ) + val defaultSince = TimeUtils.oneWeekAgo() + + return followsSet.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + val relay = it.key + + listOfNotNull( + it.value.authors?.let { + filterContentDVMsAuthors(relay, it, since) + }, + it.value.geotags?.let { + filterContentDVMsByGeohash(relay, it, since) + }, + it.value.hashtags?.let { + filterContentDVMsByHashtag(relay, it, since) + }, + it.value.communities?.let { + filterContentDVMsAllCommunities(relay, it, since) + }, + ).flatten() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt index 9b04759007..0f8a6e7de6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt @@ -20,25 +20,29 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterContentDVMsByGeohash( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + geotags: Set, + since: Long? = null, +): List { + if (geotags.isEmpty()) return emptyList() - val geoHashes = hashToLoad.toList() + val geoHashes = geotags.sorted() return listOf( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( + Filter( kinds = listOf(AppDefinitionEvent.KIND), tags = mapOf("k" to listOf("5300"), "g" to geoHashes), limit = 300, @@ -47,3 +51,24 @@ fun filterContentDVMsByGeohash( ), ) } + +fun filterContentDVMsByGeohash( + geoSet: LocationTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (geoSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return geoSet.set.mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterContentDVMsByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt index 076b9d6d34..1a486d0118 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt @@ -20,26 +20,30 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterContentDVMsByHashtag( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + hashtags: Set, + since: Long?, +): List { + if (hashtags.isEmpty()) return emptyList() - val hashtags = hashtagAlts(hashToLoad).toList() + val hashtags = hashtags.flatMap(::hashtagAlts).distinct().sorted() return listOf( - TypedFilter( - types = setOf(FeedType.PUBLIC_CHATS), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( + Filter( kinds = listOf(AppDefinitionEvent.KIND), tags = mapOf("k" to listOf("5300"), "t" to hashtags), limit = 300, @@ -48,3 +52,24 @@ fun filterContentDVMsByHashtag( ), ) } + +fun filterContentDVMsByHashtag( + hashSet: HashtagTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (hashSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return hashSet.set.mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterContentDVMsByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt new file mode 100644 index 0000000000..2f44bd1999 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils + +fun filterContentDVMsGlobal( + relays: GlobalTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (relays.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return relays.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + listOf( + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = listOf(AppDefinitionEvent.KIND), + tags = mapOf("k" to listOf("5300")), + limit = 300, + since = since, + ), + ), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt index ecc5fb9971..2f62564a0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt @@ -55,11 +55,9 @@ open class DiscoverMarketplaceFeedFilter( override fun applyFilter(collection: Set): Set = innerApplyFilter(collection) fun buildFilterParams(account: Account): FilterByListParams = - FilterByListParams.Companion.create( - account.userProfile().pubkeyHex, - account.settings.defaultDiscoveryFollowList.value, + FilterByListParams.create( account.liveDiscoveryFollowLists.value, - account.flowHiddenUsers.value, + account.hiddenUsers.flow.value, ) protected open fun innerApplyFilter(collection: Collection): Set { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 9466877f0f..2332b3f414 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -60,7 +60,7 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialogEasy +import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -96,7 +96,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.launch @@ -212,10 +211,10 @@ fun NewProductScreen( }, ) { pad -> if (showRelaysDialog) { - RelaySelectionDialogEasy( + RelaySelectionDialog( preSelectedList = postViewModel.relayList ?: persistentListOf(), onClose = { showRelaysDialog = false }, - onPost = { postViewModel.relayList = it.map { it.url }.toImmutableList() }, + onPost = { postViewModel.relayList = it }, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index d9fce20a7e..56ce892d31 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash @@ -164,7 +165,7 @@ open class NewProductViewModel : var wantsZapraiser by mutableStateOf(false) override val zapRaiserAmount = mutableStateOf(null) - var relayList by mutableStateOf?>(null) + var relayList by mutableStateOf?>(null) fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() @@ -474,17 +475,7 @@ open class NewProductViewModel : fun reloadRelaySet() { val account = accountViewModel?.account ?: return - val nip65 = account.normalizedNIP65WriteRelayList.value - val private = account.normalizedPrivateOutBoxRelaySet.value - val local = account.settings.localRelayServers - - relayList = - if (nip65.isEmpty()) { - account.activeWriteRelays().map { it.url }.toImmutableList() - } else { - val combined: Set = (nip65 + private + local) - combined.toImmutableList() - } + relayList = account.outboxRelays.flow.value.toImmutableList() } fun deleteMediaToUpload(selected: SelectedMediaProcessing) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt new file mode 100644 index 0000000000..b4047919a0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds + +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByAllCommunities +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByAuthors +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByCommunity +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsGlobal +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter + +fun makeClassifiedsFilter( + feedSettings: IFeedTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + return when (feedSettings) { + is AllCommunitiesTopNavPerRelayFilterSet -> filterClassifiedsByAllCommunities(feedSettings, since) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByFollows(feedSettings, since) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since) + is GlobalTopNavPerRelayFilterSet -> filterClassifiedsGlobal(feedSettings, since) + is HashtagTopNavPerRelayFilterSet -> filterClassifiedsByHashtag(feedSettings, since) + is LocationTopNavPerRelayFilterSet -> filterClassifiedsByGeohash(feedSettings, since) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since) + is SingleCommunityTopNavPerRelayFilterSet -> filterClassifiedsByCommunity(feedSettings, since) + else -> emptyList() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt new file mode 100644 index 0000000000..d143e23bc4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterClassifiedsAllCommunities( + relay: NormalizedRelayUrl, + communities: Set, + since: Long? = null, +): List { + val communityList = communities.sorted() + + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to communityList, + "k" to listOf(ClassifiedsEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = mapOf("a" to communityList), + kinds = listOf(ClassifiedsEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterClassifiedsByAllCommunities( + communitySet: AllCommunitiesTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterClassifiedsAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt new file mode 100644 index 0000000000..c6168c634f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull + +fun filterClassifiedsAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + val authorList = authors.sorted() + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authorList, + kinds = listOf(ClassifiedsEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterClassifiedsByAuthors( + authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterClassifiedsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} + +fun filterClassifiedsByAuthors( + authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterClassifiedsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt new file mode 100644 index 0000000000..7b5d64e708 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterClassifiedsByCommunity( + relay: NormalizedRelayUrl, + community: String, + authors: Set?, + since: Long? = null, +): List { + val authors = authors?.sorted() + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to listOf(community), + "k" to listOf(ClassifiedsEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + tags = mapOf("a" to listOf(community)), + kinds = listOf(ClassifiedsEvent.KIND), + limit = 300, + since = since, + ), + ), + ) +} + +fun filterClassifiedsByCommunity( + communitySet: SingleCommunityTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterClassifiedsByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt index 38c5d3c8f0..0a6924668f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt @@ -20,29 +20,37 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten fun filterClassifiedsByFollows( - follows: Map>?, - since: Map?, -): List? { - if (follows != null && follows.isEmpty()) return null + followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (followsSet.set.isEmpty()) return emptyList() - return listOf( - TypedFilter( - types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(ClassifiedsEvent.KIND), - limit = 300, - since = since, - ), - ), - ) + val defaultSince = TimeUtils.oneWeekAgo() + + return followsSet.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + val relay = it.key + + listOfNotNull( + it.value.authors?.let { + filterClassifiedsAuthors(relay, it, since) + }, + it.value.geotags?.let { + filterClassifiedsByGeohash(relay, it, since) + }, + it.value.hashtags?.let { + filterClassifiedsByHashtag(relay, it, since) + }, + it.value.communities?.let { + filterClassifiedsAllCommunities(relay, it, since) + }, + ).flatten() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt index 9273b0028d..f43b661216 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt @@ -20,25 +20,29 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterClassifiedsByGeohash( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + geotags: Set, + since: Long? = null, +): List { + if (geotags.isEmpty()) return emptyList() - val geoHashes = hashToLoad.toList() + val geoHashes = geotags.sorted() return listOf( - TypedFilter( - types = setOf(FeedType.GLOBAL), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( + Filter( kinds = listOf(ClassifiedsEvent.KIND), tags = mapOf("g" to geoHashes), limit = 300, @@ -47,3 +51,24 @@ fun filterClassifiedsByGeohash( ), ) } + +fun filterClassifiedsByGeohash( + geoSet: LocationTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (geoSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return geoSet.set.mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterClassifiedsByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt index a82f424ea6..5efa7423d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt @@ -20,26 +20,30 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull fun filterClassifiedsByHashtag( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + hashtags: Set?, + since: Long?, +): List? { + if (hashtags == null || hashtags.isEmpty()) return null - val hashtags = hashtagAlts(hashToLoad).toList() + val hashtags = hashtags.flatMap(::hashtagAlts).distinct().sorted() return listOf( - TypedFilter( - types = setOf(FeedType.GLOBAL), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( + Filter( kinds = listOf(ClassifiedsEvent.KIND), tags = mapOf("t" to hashtags), limit = 300, @@ -48,3 +52,24 @@ fun filterClassifiedsByHashtag( ), ) } + +fun filterClassifiedsByHashtag( + hashSet: HashtagTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (hashSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return hashSet.set.mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterClassifiedsByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt new file mode 100644 index 0000000000..57c0f48397 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies + +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils + +fun filterClassifiedsGlobal( + relays: GlobalTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (relays.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return relays.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + listOf( + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = listOf(ClassifiedsEvent.KIND), + limit = 300, + since = since, + ), + ), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index cd8da2f71a..4f66b17bd3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -151,7 +151,7 @@ fun DvmContentDiscoveryScreen( } val onRefresh = { - accountViewModel.requestDVMContentDiscovery(noteAuthor.pubkeyHex) { + accountViewModel.requestDVMContentDiscovery(noteAuthor) { requestEventID = it } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt index b5ab39471b..98fc314c5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt @@ -73,10 +73,8 @@ open class NIP90ContentDiscoveryResponseFilter( fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( - account.userProfile().pubkeyHex, - account.settings.defaultDiscoveryFollowList.value, account.liveDiscoveryFollowLists.value, - account.flowHiddenUsers.value, + account.hiddenUsers.flow.value, ) protected open fun innerApplyFilter(collection: Collection): Set { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt index 05bedb1b83..aff0303d24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash +import android.annotation.SuppressLint import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -33,6 +34,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView @@ -44,25 +46,31 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton @Composable fun GeoHashScreen( - tag: String?, + tag: Route.Geohash, accountViewModel: AccountViewModel, nav: INav, ) { - if (tag == null) return + if (tag.geohash.isEmpty()) return PrepareViewModelsGeoHashScreen(tag, accountViewModel, nav) } +@SuppressLint("StateFlowValueCalledInComposition") @Composable fun PrepareViewModelsGeoHashScreen( - tag: String, + tag: Route.Geohash, accountViewModel: AccountViewModel, nav: INav, ) { val geohashViewModel: GeoHashFeedViewModel = viewModel( - key = tag + "GeoHashFeedViewModel", - factory = GeoHashFeedViewModel.Factory(tag, accountViewModel.account), + key = tag.geohash + "GeoHashFeedViewModel", + factory = + GeoHashFeedViewModel.Factory( + tag.geohash, + accountViewModel.account.followOutboxes.flow.value, + accountViewModel.account, + ), ) GeoHashScreen(tag, geohashViewModel, accountViewModel, nav) @@ -70,27 +78,27 @@ fun PrepareViewModelsGeoHashScreen( @Composable fun GeoHashScreen( - tag: String, + tag: Route.Geohash, feedViewModel: GeoHashFeedViewModel, accountViewModel: AccountViewModel, nav: INav, ) { WatchLifecycleAndUpdateModel(feedViewModel) - GeoHashFilterAssemblerSubscription(tag, accountViewModel.dataSources().geohashes) + GeoHashFilterAssemblerSubscription(tag, accountViewModel) DisappearingScaffold( isInvertedLayout = false, topBar = { TopBarExtensibleWithBackButton( title = { - DisplayGeoTagHeader(tag, Modifier.weight(1f)) - GeoHashActionOptions(tag, accountViewModel) + DisplayGeoTagHeader(tag.geohash, Modifier.weight(1f)) + GeoHashActionOptions(tag.geohash, accountViewModel) }, popBack = nav::popBack, ) }, floatingButton = { - NewGeoPostButton(tag, accountViewModel, nav) + NewGeoPostButton(tag.geohash, accountViewModel, nav) }, accountViewModel = accountViewModel, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt index cf5e46e1e3..1ffdfaefd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -39,6 +40,7 @@ import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId class GeoHashFeedFilter( val tag: String, + val relays: Set, val account: Account, val cache: LocalCache, ) : AdditiveFeedFilter() { @@ -62,7 +64,7 @@ class GeoHashFeedFilter( geoTag: String, ): Boolean = (acceptableViaHashtag(it.event, geoTag) || acceptableViaScope(it.event, geoTag)) && - !it.isHiddenFor(account.flowHiddenUsers.value) && + !it.isHiddenFor(account.hiddenUsers.flow.value) && account.isAcceptable(it) fun acceptableViaHashtag( @@ -77,8 +79,7 @@ class GeoHashFeedFilter( event is PrivateDmEvent || event is PollNoteEvent || event is AudioHeaderEvent - ) && - event.isTaggedGeoHash(geohash) == true + ) && event.isTaggedGeoHash(geohash) fun acceptableViaScope( event: Event?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt index 768dc650a9..bd754f7a24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedViewModel.kt @@ -26,20 +26,23 @@ import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable class GeoHashFeedViewModel( val geohash: String, + val relays: Set, val account: Account, ) : FeedViewModel( - GeoHashFeedFilter(geohash, account, LocalCache), + GeoHashFeedFilter(geohash, relays, account, LocalCache), ) { @Suppress("UNCHECKED_CAST") class Factory( val geohash: String, + val relays: Set, val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = GeoHashFeedViewModel(geohash, account) as T + override fun create(modelClass: Class): T = GeoHashFeedViewModel(geohash, relays, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt index ec91356dc6..8df6e09d09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt @@ -20,13 +20,14 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.CommentKinds import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent @@ -36,44 +37,51 @@ import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +val PostsByGeohashKinds = + listOf( + TextNoteEvent.KIND, + ChannelMessageEvent.KIND, + LongTextNoteEvent.KIND, + PollNoteEvent.KIND, + ClassifiedsEvent.KIND, + HighlightEvent.KIND, + AudioTrackEvent.KIND, + AudioHeaderEvent.KIND, + WikiNoteEvent.KIND, + CommentEvent.KIND, + ) + fun filterPostsByGeohash( geohash: String, - since: Map?, -): List = - listOf( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("g" to listOf(geohash)), - kinds = - listOf( - TextNoteEvent.KIND, - ChannelMessageEvent.KIND, - LongTextNoteEvent.KIND, - PollNoteEvent.KIND, - ClassifiedsEvent.KIND, - HighlightEvent.KIND, - AudioTrackEvent.KIND, - AudioHeaderEvent.KIND, - WikiNoteEvent.KIND, - CommentEvent.KIND, - ), - limit = 200, - since = since, - ), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("I" to listOf(GeohashId.toScope(geohash))), - kinds = - listOf( - CommentEvent.KIND, - ), - limit = 200, - since = since, - ), - ), - ) + relays: Set, + since: SincePerRelayMap?, +): List { + val geohashesToFollowMap = mapOf("g" to listOf(geohash)) + val geohashesScoreMap = mapOf("I" to listOf(GeohashId.toScope(geohash))) + + return relays.flatMap { relay -> + val since = since?.get(relay)?.time + listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = geohashesToFollowMap, + kinds = PostsByGeohashKinds, + limit = 200, + since = since, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = geohashesScoreMap, + kinds = CommentKinds, + limit = 200, + since = since, + ), + ), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt index c8cab8525d..bd4d569244 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class GeoHashFeedFilterSubAssembler( client: NostrClient, @@ -31,8 +31,8 @@ class GeoHashFeedFilterSubAssembler( ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: GeohashQueryState, - since: Map?, - ): List? = filterPostsByGeohash(key.geohash, since) + since: SincePerRelayMap?, + ): List? = filterPostsByGeohash(key.geohash, key.relays, since) /** * Only one key per hashtag. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt index 3764b2fb53..088fbe0daa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt @@ -22,11 +22,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl // This allows multiple screen to be listening to tags, even the same tag class GeohashQueryState( val geohash: String, + val relays: Set, ) { val lowercaseGeohash = geohash.lowercase() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt index 62da067bb7..27b70f85a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt @@ -20,21 +20,25 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource +import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +@SuppressLint("StateFlowValueCalledInComposition") @Composable fun GeoHashFilterAssemblerSubscription( - tag: String, - dataSource: GeoHashFilterAssembler, + tag: Route.Geohash, + accountViewModel: AccountViewModel, ) { // different screens get different states // even if they are tracking the same tag. val state = remember(tag) { - GeohashQueryState(tag) + GeohashQueryState(tag.geohash, accountViewModel.account.followOutboxes.flow.value) } - KeyDataSourceSubscription(state, dataSource) + KeyDataSourceSubscription(state, accountViewModel.dataSources().geohashes) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt index fb306881a9..7a87339180 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag +import android.annotation.SuppressLint import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -38,6 +39,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -49,27 +51,29 @@ import com.vitorpamplona.amethyst.ui.theme.StdPadding @Composable fun HashtagScreen( - tag: String?, + tag: Route.Hashtag, accountViewModel: AccountViewModel, nav: INav, ) { - if (tag == null) return + if (tag.hashtag.isEmpty()) return PrepareViewModelsHashtagScreen(tag, accountViewModel, nav) } +@SuppressLint("StateFlowValueCalledInComposition") @Composable fun PrepareViewModelsHashtagScreen( - tag: String, + tag: Route.Hashtag, accountViewModel: AccountViewModel, nav: INav, ) { val hashtagFeedViewModel: HashtagFeedViewModel = viewModel( - key = tag + "HashtagFeedViewModel", + key = tag.hashtag + "HashtagFeedViewModel", factory = HashtagFeedViewModel.Factory( - tag, + tag.hashtag, + accountViewModel.account.followOutboxes.flow.value, accountViewModel.account, ), ) @@ -79,27 +83,27 @@ fun PrepareViewModelsHashtagScreen( @Composable fun HashtagScreen( - tag: String, + tag: Route.Hashtag, feedViewModel: HashtagFeedViewModel, accountViewModel: AccountViewModel, nav: INav, ) { WatchLifecycleAndUpdateModel(feedViewModel) - HashtagFilterAssemblerSubscription(tag, accountViewModel.dataSources().hashtags) + HashtagFilterAssemblerSubscription(tag, accountViewModel) DisappearingScaffold( isInvertedLayout = false, topBar = { TopBarExtensibleWithBackButton( title = { - Text("#$tag", modifier = Modifier.weight(1f)) - HashtagActionOptions(tag, accountViewModel) + Text("#${tag.hashtag}", modifier = Modifier.weight(1f)) + HashtagActionOptions(tag.hashtag, accountViewModel) }, popBack = nav::popBack, ) }, floatingButton = { - NewHashtagPostButton(tag, accountViewModel, nav) + NewHashtagPostButton(tag.hashtag, accountViewModel, nav) }, accountViewModel = accountViewModel, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt index 265c8bf3f1..be5a8f4834 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -41,6 +42,7 @@ import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId class HashtagFeedFilter( val tag: String, + val relays: Set, val account: Account, val cache: LocalCache, ) : AdditiveFeedFilter() { @@ -64,7 +66,7 @@ class HashtagFeedFilter( hashTag: String, ): Boolean = (acceptableViaHashtag(it.event, hashTag) || acceptableViaScope(it.event, hashTag)) && - !it.isHiddenFor(account.flowHiddenUsers.value) && + !it.isHiddenFor(account.hiddenUsers.flow.value) && account.isAcceptable(it) fun acceptableViaHashtag( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt index 07a5c3cac3..b1b4879417 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedViewModel.kt @@ -26,20 +26,23 @@ import androidx.lifecycle.ViewModelProvider import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.FeedViewModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable class HashtagFeedViewModel( val hashtag: String, + val relays: Set, val account: Account, ) : FeedViewModel( - HashtagFeedFilter(hashtag, account, LocalCache), + HashtagFeedFilter(hashtag, relays, account, LocalCache), ) { @Suppress("UNCHECKED_CAST") class Factory( val hashtag: String, + val relays: Set, val account: Account, ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = HashtagFeedViewModel(hashtag, account) as T + override fun create(modelClass: Class): T = HashtagFeedViewModel(hashtag, relays, account) as T } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt index d5983195d2..5b0c37659b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt @@ -20,14 +20,15 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.CommentKinds import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent @@ -39,58 +40,67 @@ import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +val PostsByHashtagsKinds = + listOf( + TextNoteEvent.KIND, + ChannelMessageEvent.KIND, + LongTextNoteEvent.KIND, + PollNoteEvent.KIND, + LiveActivitiesChatMessageEvent.KIND, + ClassifiedsEvent.KIND, + HighlightEvent.KIND, + WikiNoteEvent.KIND, + CommentEvent.KIND, + ) + +val PostsByHashtagKinds2 = + listOf( + InteractiveStorySceneEvent.KIND, + AudioTrackEvent.KIND, + AudioHeaderEvent.KIND, + ) + fun filterPostsByHashtags( hashtag: String, - since: Map?, -): List { - val hashtagsToFollow = hashtagAlts(hashtag).toList() + relays: Set, + since: SincePerRelayMap?, +): List { + val hashtagsToFollowMap = mapOf("t" to hashtagAlts(hashtag).sorted()) + val hashtagScoreMap = mapOf("I" to listOf(HashtagId.toScope(hashtag))) - return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("t" to hashtagsToFollow), - kinds = - listOf( - TextNoteEvent.KIND, - ChannelMessageEvent.KIND, - LongTextNoteEvent.KIND, - PollNoteEvent.KIND, - LiveActivitiesChatMessageEvent.KIND, - ClassifiedsEvent.KIND, - HighlightEvent.KIND, - WikiNoteEvent.KIND, - CommentEvent.KIND, - ), - limit = 400, - since = since, - ), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("t" to hashtagsToFollow), - kinds = - listOf( - InteractiveStorySceneEvent.KIND, - AudioTrackEvent.KIND, - AudioHeaderEvent.KIND, - ), - limit = 100, - since = since, - ), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("I" to listOf(HashtagId.toScope(hashtag))), - kinds = listOf(CommentEvent.KIND), - limit = 200, - since = since, - ), - ), - ) + return relays.flatMap { relay -> + val since = since?.get(relay)?.time + listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = hashtagsToFollowMap, + kinds = PostsByHashtagsKinds, + limit = 400, + since = since, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = hashtagsToFollowMap, + kinds = PostsByHashtagKinds2, + limit = 100, + since = since, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = hashtagScoreMap, + kinds = CommentKinds, + limit = 200, + since = since, + ), + ), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt index 5ceaa226ac..81668475c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class HashtagFeedFilterSubAssembler( client: NostrClient, @@ -31,8 +31,8 @@ class HashtagFeedFilterSubAssembler( ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: HashtagQueryState, - since: Map?, - ): List = filterPostsByHashtags(key.hashtag, since) + since: SincePerRelayMap?, + ): List = filterPostsByHashtags(key.hashtag, key.relays, since) /** * Only one key per hashtag. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt index 87319df30a..f21cce3220 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt @@ -21,11 +21,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl // This allows multiple screen to be listening to tags, even the same tag class HashtagQueryState( val hashtag: String, + val relays: Set, ) { val lowercaseHashtag = hashtag.lowercase() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt index 665e1d69ad..51c1c20481 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt @@ -20,21 +20,24 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource +import android.R.attr.tag import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable fun HashtagFilterAssemblerSubscription( - tag: String, - dataSource: HashtagFilterAssembler, + tag: Route.Hashtag, + accountViewModel: AccountViewModel, ) { // different screens get different states // even if they are tracking the same tag. val state = remember(tag) { - HashtagQueryState(tag) + HashtagQueryState(tag.hashtag, accountViewModel.account.followOutboxes.flow.value) } - KeyDataSourceSubscription(state, dataSource) + KeyDataSourceSubscription(state, accountViewModel.dataSources().hashtags) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index b3909a8b68..356bef90ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -50,7 +50,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically @@ -63,7 +62,7 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialogEasy +import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -241,10 +240,10 @@ private fun NewPostScreenInner( }, ) { pad -> if (postViewModel.showRelaysDialog) { - RelaySelectionDialogEasy( + RelaySelectionDialog( preSelectedList = postViewModel.relayList ?: persistentListOf(), onClose = { postViewModel.showRelaysDialog = false }, - onPost = { postViewModel.relayList = it.map { it.url }.toImmutableList() }, + onPost = { postViewModel.relayList = it }, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 00f4996d58..2ef0869e05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -74,6 +74,7 @@ import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash @@ -215,7 +216,7 @@ open class ShortNotePostViewModel : override val zapRaiserAmount = mutableStateOf(null) var showRelaysDialog by mutableStateOf(false) - var relayList by mutableStateOf?>(null) + var relayList by mutableStateOf?>(null) fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() @@ -471,11 +472,11 @@ open class ShortNotePostViewModel : val template = createTemplate() ?: return val relayList = relayList - if (nip95attachments.isNotEmpty() && relayList != null) { + if (nip95attachments.isNotEmpty() && relayList != null && relayList.isNotEmpty()) { val usedImages = template.tags.taggedQuoteIds().toSet() nip95attachments.forEach { - if (usedImages.contains(it.second.id) == true) { - account?.sendNip95Privately(it.first, it.second, relayList) + if (usedImages.contains(it.second.id)) { + account?.sendNip95(it.first, it.second, relayList.toSet()) } } } @@ -715,17 +716,7 @@ open class ShortNotePostViewModel : fun reloadRelaySet() { val account = accountViewModel?.account ?: return - val nip65 = account.normalizedNIP65WriteRelayList.value - val private = account.normalizedPrivateOutBoxRelaySet.value - val local = account.settings.localRelayServers - - relayList = - if (nip65.isEmpty()) { - account.activeWriteRelays().map { it.url }.toImmutableList() - } else { - val combined: Set = (nip65 + private + local) - combined.toImmutableList() - } + relayList = account.outboxRelays.flow.value.toImmutableList() } fun deleteMediaToUpload(selected: SelectedMediaProcessing) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt index 4a1cedc051..81e4ecc14f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams @@ -31,8 +32,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent class HomeConversationsFeedFilter( @@ -40,9 +39,7 @@ class HomeConversationsFeedFilter( ) : AdditiveFeedFilter() { override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultHomeFollowList.value - override fun showHiddenKey(): Boolean = - account.settings.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || - account.settings.defaultHomeFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) + override fun showHiddenKey(): Boolean = account.liveHomeFollowLists.value is MutedAuthorsByOutboxTopNavFilter override fun feed(): List { val filterParams = buildFilterParams(account) @@ -58,10 +55,8 @@ class HomeConversationsFeedFilter( fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( - userHex = account.userProfile().pubkeyHex, - selectedListName = account.settings.defaultHomeFollowList.value, followLists = account.liveHomeFollowLists.value, - hiddenUsers = account.flowHiddenUsers.value, + hiddenUsers = account.hiddenUsers.flow.value, ) private fun innerApplyFilter(collection: Collection): Set { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 3806261a89..e4b15204ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -25,6 +25,10 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveComplexFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent @@ -41,42 +45,32 @@ class HomeLiveFilter( fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( - userHex = account.userProfile().pubkeyHex, - selectedListName = account.settings.defaultHomeFollowList.value, followLists = account.liveHomeFollowLists.value, - hiddenUsers = account.flowHiddenUsers.value, + hiddenUsers = account.hiddenUsers.flow.value, ) fun limitTime() = TimeUtils.fifteenMinutesAgo() override fun feed(): List { - val gRelays = account.activeGlobalRelays().toSet() val filterParams = buildFilterParams(account) val fiveMinsAgo = limitTime() val list = - LocalCache.channels.filter { id, channel -> - shouldIncludeChannel(channel, gRelays, filterParams, fiveMinsAgo) + LocalCache.ephemeralChannels.filter { id, channel -> + shouldIncludeChannel(channel, filterParams, fiveMinsAgo) } return sort(list.toSet()) } fun shouldIncludeChannel( - channel: Channel, - gRelays: Set, + channel: EphemeralChatChannel, filterParams: FilterByListParams, timeLimit: Long, ): Boolean = - if (channel is EphemeralChatChannel) { - val list = - channel.notes.filter { key, value -> - acceptableEvent(value, gRelays, filterParams, timeLimit) - } - list.isNotEmpty() - } else { - false - } + channel.notes.filter { key, value -> + acceptableEvent(value, filterParams, timeLimit) + }.isNotEmpty() override fun updateListWith( oldList: List, @@ -110,31 +104,37 @@ class HomeLiveFilter( } private fun applyFilter(collection: Collection): Set { - val gRelays = account.activeGlobalRelays().toSet() val filterParams = buildFilterParams(account) return collection.filterTo(HashSet()) { - acceptableEvent(it, gRelays, filterParams, limitTime()) + acceptableEvent(it, filterParams, limitTime()) } } private fun acceptableEvent( - it: Note, - globalRelays: Set, + note: Note, filterParams: FilterByListParams, timeLimit: Long, ): Boolean { - val createdAt = it.createdAt() ?: return false - val noteEvent = it.event - val isGlobalRelay = it.relays.any { globalRelays.contains(it.url) } + val createdAt = note.createdAt() ?: return false + val noteEvent = note.event return (noteEvent is EphemeralChatEvent) && createdAt > timeLimit && - filterParams.match(noteEvent, isGlobalRelay) + filterParams.match(noteEvent, note.relays) } fun sort(collection: Set): List { - val followingKeySet = - account.liveDiscoveryFollowLists.value?.authors ?: account.liveKind3Follows.value.authors + val topFilter = account.liveDiscoveryFollowLists.value + val topFilterAuthors = + when (topFilter) { + is AuthorsByOutboxTopNavFilter -> topFilter.authors + is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors + is AllFollowsByOutboxTopNavFilter -> topFilter.authors + is SingleCommunityTopNavFilter -> topFilter.authors + else -> null + } + + val followingKeySet = topFilterAuthors ?: account.kind3FollowList.flow.value.authors val followCounts = collection.associate { it to followsThatParticipateOn(it, followingKeySet) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index ebd51792e9..fd0d0e67e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal -import androidx.compose.ui.util.fastAny import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams @@ -36,8 +36,6 @@ import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent @@ -47,31 +45,26 @@ class HomeNewThreadFeedFilter( ) : AdditiveFeedFilter() { override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultHomeFollowList.value - override fun showHiddenKey(): Boolean = - account.settings.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || - account.settings.defaultHomeFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) + override fun showHiddenKey(): Boolean = account.liveHomeFollowLists.value is MutedAuthorsByOutboxTopNavFilter fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( - userHex = account.userProfile().pubkeyHex, - selectedListName = account.settings.defaultHomeFollowList.value, followLists = account.liveHomeFollowLists.value, - hiddenUsers = account.flowHiddenUsers.value, + hiddenUsers = account.hiddenUsers.flow.value, ) override fun feed(): List { - val gRelays = account.activeGlobalRelays().toSet() val filterParams = buildFilterParams(account) val notes = LocalCache.notes.filterIntoSet { _, note -> // Avoids processing addressables twice. - (note.event?.kind ?: 99999) < 10000 && acceptableEvent(note, gRelays, filterParams) + (note.event?.kind ?: 99999) < 10000 && acceptableEvent(note, filterParams) } val longFormNotes = LocalCache.addressables.filterIntoSet { _, note -> - acceptableEvent(note, gRelays, filterParams) + acceptableEvent(note, filterParams) } return sort(notes + longFormNotes) @@ -80,21 +73,18 @@ class HomeNewThreadFeedFilter( override fun applyFilter(collection: Set): Set = innerApplyFilter(collection) private fun innerApplyFilter(collection: Collection): Set { - val gRelays = account.activeGlobalRelays().toSet() val filterParams = buildFilterParams(account) return collection.filterTo(HashSet()) { - acceptableEvent(it, gRelays, filterParams) + acceptableEvent(it, filterParams) } } private fun acceptableEvent( it: Note, - globalRelays: Set, filterParams: FilterByListParams, ): Boolean { val noteEvent = it.event - val isGlobalRelay = it.relays.fastAny { globalRelays.contains(it.url) } return ( noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || @@ -109,7 +99,7 @@ class HomeNewThreadFeedFilter( noteEvent is AudioTrackEvent || noteEvent is AudioHeaderEvent ) && - filterParams.match(noteEvent, isGlobalRelay) && + filterParams.match(noteEvent, it.relays) && it.isNewThread() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt index aab0c0375b..06638abdce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt @@ -22,10 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.mixGeohashHashtagsCommunities.MixGeohashHashtagsCommunityEoseManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomeOutboxEventsEoseManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomeOutboxUsersEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import kotlinx.coroutines.CoroutineScope // This allows multiple screen to be listening to tags, even the same tag @@ -40,12 +38,12 @@ class HomeFilterAssembler( val group = listOf( HomeOutboxEventsEoseManager(client, ::allKeys), - HomeOutboxUsersEoseManager(client, ::allKeys), + // HomeOutboxUsersEoseManager(client, ::allKeys), // We can break it down, one for each sub if needed. // HashtagEventsFilterSubAssembler(client, ::allKeys), // GeohashEventsFilterSubAssembler(client, ::allKeys), // CommunityEventsFilterSubAssembler(client, ::allKeys), - MixGeohashHashtagsCommunityEoseManager(client, ::allKeys), + // MixGeohashHashtagsCommunityEoseManager(client, ::allKeys), ) override fun start() = group.forEach { it.start() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/mixGeohashHashtagsCommunities/MixGeohashHashtagsCommunityEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/mixGeohashHashtagsCommunities/MixGeohashHashtagsCommunityEoseManager.kt deleted file mode 100644 index eb7f79e677..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/mixGeohashHashtagsCommunities/MixGeohashHashtagsCommunityEoseManager.kt +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.mixGeohashHashtagsCommunities - -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Geohash.filterHomePostsByGeohashes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Hashtags.filterHomePostsByHashtags -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsFromCommunities -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -class MixGeohashHashtagsCommunityEoseManager( - client: NostrClient, - allKeys: () -> Set, -) : PerUserEoseManager(client, allKeys) { - override fun updateFilter( - key: HomeQueryState, - since: Map?, - ): List? = - listOfNotNull( - filterHomePostsByHashtags(key.followLists()?.hashtags, since), - filterHomePostsByGeohashes(key.followLists()?.geotags, since), - filterHomePostsByScopes(key.followLists()?.hashtagScopes, since), - filterHomePostsByScopes(key.followLists()?.geotagScopes, since), - filterHomePostsFromCommunities(key.followLists()?.addresses, since), - ).flatten() - - override fun user(query: HomeQueryState) = query.account.userProfile() - - fun HomeQueryState.followListsFlow() = account.liveHomeFollowLists - - fun HomeQueryState.followLists() = followListsFlow().value - - val userJobMap = mutableMapOf() - - override fun newSub(key: HomeQueryState): Subscription { - val user = user(key) - userJobMap[user]?.cancel() - userJobMap[user] = - key.scope.launch(Dispatchers.Default) { - key.followListsFlow().collectLatest { - invalidateFilters() - } - } - - return super.newSub(key) - } - - override fun endSub( - key: User, - subId: String, - ) { - return super.endSub(key, subId) - userJobMap[key]?.cancel() - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt new file mode 100644 index 0000000000..6297505617 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core + +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull +import kotlin.collections.sorted + +val HomePostsByGeohashKinds = + listOf( + TextNoteEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + LongTextNoteEvent.KIND, + ClassifiedsEvent.KIND, + HighlightEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + WikiNoteEvent.KIND, + CommentEvent.KIND, + ) + +fun filterHomePostsByGeohashes( + relay: NormalizedRelayUrl, + geotags: Set, + since: Long, +): List { + if (geotags.isEmpty()) return emptyList() + + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = HomePostsByGeohashKinds, + tags = mapOf("g" to geotags.sorted()), + limit = 100, + since = since, + ), + ), + ) +} + +fun filterHomePostsByGeohashes( + geoSet: LocationTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (geoSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return geoSet.set.mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterHomePostsByGeohashes( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + + filterHomePostsByScopes( + relay = it.key, + scopesToLoad = it.value.geotagScopes, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt similarity index 51% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByFollows.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt index 9ae9a876c4..98d6cc60e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt @@ -18,18 +18,17 @@ * 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.home.datasource.nip65Follows +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -40,52 +39,61 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEven import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils -fun filterHomePostsByFollows( - follows: Map>?, - since: Map?, -): List { - if (follows != null && follows.isEmpty()) return emptyList() - - return listOf( - TypedFilter( - types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - ClassifiedsEvent.KIND, - LongTextNoteEvent.KIND, - EphemeralChatEvent.KIND, - HighlightEvent.KIND, - ), - authors = follows, - limit = 400, - since = since, - ), - ), - TypedFilter( - types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - kinds = - listOf( - PollNoteEvent.KIND, - AudioTrackEvent.KIND, - AudioHeaderEvent.KIND, - PinListEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - LiveActivitiesChatMessageEvent.KIND, - LiveActivitiesEvent.KIND, - WikiNoteEvent.KIND, - ), - authors = follows, - limit = 400, - since = since, - ), - ), +val HomePostsByGlobalKinds = + listOf( + TextNoteEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + ClassifiedsEvent.KIND, + LongTextNoteEvent.KIND, + EphemeralChatEvent.KIND, + HighlightEvent.KIND, ) + +val HomePostsByGlobalKinds2 = + listOf( + PollNoteEvent.KIND, + AudioTrackEvent.KIND, + AudioHeaderEvent.KIND, + PinListEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + LiveActivitiesChatMessageEvent.KIND, + LiveActivitiesEvent.KIND, + WikiNoteEvent.KIND, + ) + +fun filterHomePostsByGlobal( + relays: GlobalTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (relays.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return relays.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + val relayUrl = it.key + listOf( + RelayBasedFilter( + relay = relayUrl, + filter = + Filter( + kinds = HomePostsByGlobalKinds, + limit = 400, + since = since, + ), + ), + RelayBasedFilter( + relay = relayUrl, + filter = + Filter( + kinds = HomePostsByGlobalKinds2, + limit = 400, + since = since, + ), + ), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt new file mode 100644 index 0000000000..f7bbd8a74b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core + +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull + +val HomePostsBuHashtagsKinds = + listOf( + TextNoteEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + LongTextNoteEvent.KIND, + ClassifiedsEvent.KIND, + HighlightEvent.KIND, + AudioHeaderEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + CommentEvent.KIND, + WikiNoteEvent.KIND, + ) + +fun filterHomePostsByHashtags( + relay: NormalizedRelayUrl, + hashToLoad: Set, + since: Long?, +): List { + if (hashToLoad.isEmpty()) return emptyList() + + val hashtags = hashToLoad.flatMap { hashtagAlts(it) }.distinct().sorted() + + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = HomePostsBuHashtagsKinds, + tags = mapOf("t" to hashtags), + limit = 100, + since = since, + ), + ), + ) +} + +fun filterHomePostsByHashtags( + hashtagSet: HashtagTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (hashtagSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return hashtagSet.set.mapNotNull { + if (it.value.hashtags.isEmpty()) { + null + } else { + val since = since?.get(it.key)?.time ?: defaultSince + + return filterHomePostsByHashtags( + relay = it.key, + hashToLoad = it.value.hashtags, + since = since, + ) + + filterHomePostsByScopes( + relay = it.key, + scopesToLoad = it.value.hashtagScopes, + since = since, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/FilterPostsByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/FilterPostsByGeohashes.kt deleted file mode 100644 index 7f575c0472..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/FilterPostsByGeohashes.kt +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Geohash - -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent - -fun filterHomePostsByGeohashes( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null - - return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - LongTextNoteEvent.KIND, - ClassifiedsEvent.KIND, - HighlightEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - WikiNoteEvent.KIND, - CommentEvent.KIND, - ), - tags = mapOf("g" to hashToLoad.toList()), - limit = 100, - since = since, - ), - ), - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/GeohashEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/GeohashEventsEoseManager.kt deleted file mode 100644 index 2bbff0f7b3..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Geohash/GeohashEventsEoseManager.kt +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Geohash - -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import kotlin.collections.flatten - -class GeohashEventsEoseManager( - client: NostrClient, - allKeys: () -> Set, -) : PerUserEoseManager(client, allKeys) { - override fun updateFilter( - key: HomeQueryState, - since: Map?, - ): List? = - listOfNotNull( - filterHomePostsByGeohashes(key.followLists()?.geotags, since), - filterHomePostsByScopes(key.followLists()?.geotagScopes, since), - ).flatten() - - override fun user(query: HomeQueryState) = query.account.userProfile() - - fun HomeQueryState.followListsFlow() = account.liveHomeFollowLists - - fun HomeQueryState.followLists() = followListsFlow().value - - val userJobMap = mutableMapOf() - - override fun newSub(key: HomeQueryState): Subscription { - val user = user(key) - userJobMap[user]?.cancel() - userJobMap[user] = - key.scope.launch(Dispatchers.Default) { - key.followListsFlow().collectLatest { - invalidateFilters() - } - } - - return super.newSub(key) - } - - override fun endSub( - key: User, - subId: String, - ) { - return super.endSub(key, subId) - userJobMap[key]?.cancel() - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/FilterHomePostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/FilterHomePostsByHashtags.kt deleted file mode 100644 index ed83014e6e..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/FilterHomePostsByHashtags.kt +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Hashtags - -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip22Comments.CommentEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent - -fun filterHomePostsByHashtags( - hashToLoad: Set?, - since: Map?, -): List? { - if (hashToLoad == null || hashToLoad.isEmpty()) return null - - val hashtags = hashToLoad.map { hashtagAlts(it) }.flatten().distinct() - - return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - LongTextNoteEvent.KIND, - ClassifiedsEvent.KIND, - HighlightEvent.KIND, - AudioHeaderEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - CommentEvent.KIND, - WikiNoteEvent.KIND, - ), - tags = mapOf("t" to hashtags), - limit = 100, - since = since, - ), - ), - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/HashtagEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/HashtagEventsFilterSubAssembler.kt deleted file mode 100644 index be9be75ba1..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Hashtags/HashtagEventsFilterSubAssembler.kt +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Hashtags - -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -class HashtagEventsFilterSubAssembler( - client: NostrClient, - allKeys: () -> Set, -) : PerUserEoseManager(client, allKeys) { - override fun updateFilter( - key: HomeQueryState, - since: Map?, - ): List? = - listOfNotNull( - filterHomePostsByHashtags( - key.followLists()?.hashtags, - since, - ), - filterHomePostsByScopes(key.followLists()?.hashtagScopes, since), - ).flatten() - - override fun user(query: HomeQueryState) = query.account.userProfile() - - fun HomeQueryState.followListsFlow() = account.liveHomeFollowLists - - fun HomeQueryState.followLists() = followListsFlow().value - - val userJobMap = mutableMapOf() - - override fun newSub(key: HomeQueryState): Subscription { - val user = user(key) - userJobMap[user]?.cancel() - userJobMap[user] = - key.scope.launch(Dispatchers.Default) { - key.followListsFlow().collectLatest { - invalidateFilters() - } - } - - return super.newSub(key) - } - - override fun endSub( - key: User, - subId: String, - ) { - return super.endSub(key, subId) - userJobMap[key]?.cancel() - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByGeohashScopes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt similarity index 73% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByGeohashScopes.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt index 6a9c7b5823..04c29fb0fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByGeohashScopes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt @@ -20,24 +20,26 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip22Comments.CommentEvent +val CommentKinds = listOf(CommentEvent.KIND) + fun filterHomePostsByScopes( - scopesToLoad: Set?, - since: Map?, -): List? { - if (scopesToLoad == null || scopesToLoad.isEmpty()) return null + relay: NormalizedRelayUrl, + scopesToLoad: Set, + since: Long, +): List { + if (scopesToLoad.isEmpty()) return emptyList() return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = listOf(CommentEvent.KIND), + Filter( + kinds = CommentKinds, tags = mapOf("I" to scopesToLoad.toList()), limit = 100, since = since, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt new file mode 100644 index 0000000000..dd1f0a0212 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows + +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByGeohashes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsFromAllCommunities +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterHomePostsByAllFollows( + followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (followsSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return followsSet.set.flatMap { + val since = since?.get(it.key)?.time ?: defaultSince + val relay = it.key + + listOfNotNull( + it.value.authors?.let { + filterHomePostsByAuthors(relay, it, since) + }, + it.value.geotags?.let { + filterHomePostsByGeohashes(relay, it, since) + }, + it.value.geotagScopes?.let { + filterHomePostsByScopes(relay, it, since) + }, + it.value.hashtags?.let { + filterHomePostsByHashtags(relay, it, since) + }, + it.value.hashtagScopes?.let { + filterHomePostsByScopes(relay, it, since) + }, + it.value.communities?.let { + filterHomePostsFromAllCommunities(relay, it, since) + }, + ).flatten() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt new file mode 100644 index 0000000000..386cf8afbf --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip51Lists.PinListEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull + +val HomePostsKinds = + listOf( + TextNoteEvent.KIND, + RepostEvent.KIND, + GenericRepostEvent.KIND, + ClassifiedsEvent.KIND, + LongTextNoteEvent.KIND, + EphemeralChatEvent.KIND, + HighlightEvent.KIND, + ) + +val HomePostsKinds2 = + listOf( + PollNoteEvent.KIND, + AudioTrackEvent.KIND, + AudioHeaderEvent.KIND, + PinListEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + LiveActivitiesChatMessageEvent.KIND, + LiveActivitiesEvent.KIND, + WikiNoteEvent.KIND, + ) + +fun filterHomePostsByAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + val authorList = authors.sorted() + + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = HomePostsKinds, + authors = authorList, + limit = 400, + since = since, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = HomePostsKinds2, + authors = authorList, + limit = 400, + since = since, + ), + ), + ) +} + +fun filterHomePostsByAuthors( + authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterHomePostsByAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} + +fun filterHomePostsByAuthors( + authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterHomePostsByAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt index 08515e7dda..bbe992d11e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt @@ -21,12 +21,25 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByGeohashes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByGlobal +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsByAllCommunities +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsByCommunity import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job @@ -40,8 +53,21 @@ class HomeOutboxEventsEoseManager( ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: HomeQueryState, - since: Map?, - ): List? = filterHomePostsByFollows(key.followRelay(), since) + since: SincePerRelayMap?, + ): List? { + val feedSettings = key.followsPerRelay() + return when (feedSettings) { + is AllCommunitiesTopNavPerRelayFilterSet -> filterHomePostsByAllCommunities(feedSettings, since) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAllFollows(feedSettings, since) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAuthors(feedSettings, since) + is GlobalTopNavPerRelayFilterSet -> filterHomePostsByGlobal(feedSettings, since) + is HashtagTopNavPerRelayFilterSet -> filterHomePostsByHashtags(feedSettings, since) + is LocationTopNavPerRelayFilterSet -> filterHomePostsByGeohashes(feedSettings, since) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAuthors(feedSettings, since) + is SingleCommunityTopNavPerRelayFilterSet -> filterHomePostsByCommunity(feedSettings, since) + else -> emptyList() + } + } override fun user(key: HomeQueryState) = key.account.userProfile() @@ -51,9 +77,9 @@ class HomeOutboxEventsEoseManager( fun HomeQueryState.listName() = listNameFlow().value - fun HomeQueryState.followRelayFlow() = account.liveHomeListAuthorsPerRelay + fun HomeQueryState.followRelayFlow() = account.liveHomeFollowListsPerRelay - fun HomeQueryState.followRelay() = followRelayFlow().value + fun HomeQueryState.followsPerRelay() = followRelayFlow().value val userJobMap = mutableMapOf>() @@ -69,7 +95,7 @@ class HomeOutboxEventsEoseManager( } }, key.scope.launch(Dispatchers.Default) { - key.followRelayFlow().sample(5000).collectLatest { + key.followRelayFlow().sample(2000).collectLatest { invalidateFilters() } }, @@ -82,7 +108,7 @@ class HomeOutboxEventsEoseManager( key: User, subId: String, ) { - return super.endSub(key, subId) + super.endSub(key, subId) userJobMap[key]?.forEach { it.cancel() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/CommunityEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/CommunityEventsFilterSubAssembler.kt deleted file mode 100644 index 5d92c916c6..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/CommunityEventsFilterSubAssembler.kt +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities - -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -class CommunityEventsFilterSubAssembler( - client: NostrClient, - allKeys: () -> Set, -) : PerUserEoseManager(client, allKeys) { - override fun updateFilter( - key: HomeQueryState, - since: Map?, - ): List? = - filterHomePostsFromCommunities( - key.followLists()?.addresses, - since, - ) - - override fun user(query: HomeQueryState) = query.account.userProfile() - - fun HomeQueryState.followListsFlow() = account.liveHomeFollowLists - - fun HomeQueryState.followLists() = followListsFlow().value - - val userJobMap = mutableMapOf() - - override fun newSub(key: HomeQueryState): Subscription { - val user = user(key) - userJobMap[user]?.cancel() - userJobMap[user] = - key.scope.launch(Dispatchers.Default) { - key.followListsFlow().collectLatest { - invalidateFilters() - } - } - - return super.newSub(key) - } - - override fun endSub( - key: User, - subId: String, - ) { - return super.endSub(key, subId) - userJobMap[key]?.cancel() - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt new file mode 100644 index 0000000000..431091725c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterHomePostsFromAllCommunities( + relay: NormalizedRelayUrl, + communities: Set, + since: Long? = null, +): List { + val communityList = communities.sorted() + + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to communityList, + "k" to HomePostsFromCommunityKindsStr, + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = mapOf("a" to communityList), + kinds = HomePostsFromCommunityKinds, + limit = 300, + since = since, + ), + ), + ) +} + +fun filterHomePostsByAllCommunities( + communitySet: AllCommunitiesTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterHomePostsFromAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt index 288d209666..337a1cb124 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt @@ -20,11 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent @@ -32,36 +33,87 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull -fun filterHomePostsFromCommunities( - communitiesToLoad: Set?, - since: Map?, -): List? { - if (communitiesToLoad == null || communitiesToLoad.isEmpty()) return null +val HomePostsFromCommunityKinds = + listOf( + TextNoteEvent.KIND, + LongTextNoteEvent.KIND, + ClassifiedsEvent.KIND, + HighlightEvent.KIND, + WikiNoteEvent.KIND, + CommunityPostApprovalEvent.KIND, + CommentEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + ) +val HomePostsFromCommunityKindsStr = + listOf( + TextNoteEvent.KIND.toString(), + LongTextNoteEvent.KIND.toString(), + ClassifiedsEvent.KIND.toString(), + HighlightEvent.KIND.toString(), + WikiNoteEvent.KIND.toString(), + CommunityPostApprovalEvent.KIND.toString(), + CommentEvent.KIND.toString(), + InteractiveStoryPrologueEvent.KIND.toString(), + ) + +fun filterHomePostsFromCommunity( + relay: NormalizedRelayUrl, + community: String, + authors: Set?, + since: Long? = null, +): List { + val authors = authors?.sorted() return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), + // approved + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - LongTextNoteEvent.KIND, - ClassifiedsEvent.KIND, - HighlightEvent.KIND, - WikiNoteEvent.KIND, - CommunityPostApprovalEvent.KIND, - CommentEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - ), + Filter( + authors = authors, + kinds = CommunityPostApprovalEvent.KIND_LIST, tags = mapOf( - "a" to communitiesToLoad.toList(), + "a" to listOf(community), + "k" to HomePostsFromCommunityKindsStr, ), - limit = 100, + limit = 200, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + tags = mapOf("a" to listOf(community)), + kinds = HomePostsFromCommunityKinds, + limit = 200, since = since, ), ), ) } + +fun filterHomePostsByCommunity( + communitySet: SingleCommunityTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterHomePostsFromCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index 18ecdfd186..76ec59bb32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.dal import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter @@ -63,10 +64,8 @@ class NotificationFeedFilter( fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( - userHex = account.userProfile().pubkeyHex, - selectedListName = account.settings.defaultNotificationFollowList.value, followLists = account.liveNotificationFollowLists.value, - hiddenUsers = account.flowHiddenUsers.value, + hiddenUsers = account.hiddenUsers.flow.value, ) override fun feed(): List { @@ -108,7 +107,11 @@ class NotificationFeedFilter( noteEvent.pubKey } } else { - it.author?.pubkeyHex + if (it is AddressableNote) { + it.address.pubKeyHex + } else { + it.author?.pubkeyHex + } } return it.event !is ChannelCreateEvent && @@ -121,7 +124,7 @@ class NotificationFeedFilter( it.event !is NIP90ContentDiscoveryRequestEvent && it.event !is GiftWrapEvent && (it.event is LnZapEvent || notifAuthor != loggedInUserHex) && - (filterParams.isGlobal || filterParams.followLists?.authors?.contains(notifAuthor) == true) && + (filterParams.isGlobal(it.relays) || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor) == true) && it.event?.isTaggedUser(loggedInUserHex) ?: false && (filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) && tagsAnEventByUser(it, loggedInUserHex) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt index ae2ae24266..19ad86a314 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt @@ -20,28 +20,27 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -fun filterUserProfileFollowers( - key: HexKey, - since: Map?, -): List { - if (key.isEmpty()) return emptyList() +val UserProfileFollowersKinds = listOf(ContactListEvent.KIND) - return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, +fun filterUserProfileFollowers( + user: User, + since: SincePerRelayMap?, +): List { + return user.inboxRelays().map { + RelayBasedFilter( + relay = it, filter = - SincePerRelayFilter( - kinds = listOf(ContactListEvent.KIND), - tags = mapOf("p" to listOf(key)), - since = since, + Filter( + kinds = UserProfileFollowersKinds, + tags = mapOf("p" to listOf(user.pubkeyHex)), + since = since?.get(it)?.time, ), - ), - ) + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt index 84ea68fcb0..e1ebad1cb6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt @@ -20,38 +20,37 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent -fun filterUserProfileLists( - keys: Set, - since: Map?, -): List { - if (keys.isEmpty()) return emptyList() - - return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - BookmarkListEvent.KIND, - PeopleListEvent.KIND, - FollowListEvent.KIND, - AppRecommendationEvent.KIND, - ), - authors = keys.toList(), - limit = 100, - since = since, - ), - ), +val UserProfileListKinds = + listOf( + BookmarkListEvent.KIND, + PeopleListEvent.KIND, + FollowListEvent.KIND, + AppRecommendationEvent.KIND, ) + +fun filterUserProfileLists( + users: Map>, + since: SincePerRelayMap?, +): List { + return users.map { + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = UserProfileListKinds, + authors = it.value.sorted(), + limit = 100, + since = since?.get(it.key)?.time, + ), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt index f0ac100d13..80dc2ec2c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt @@ -20,38 +20,37 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent -import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -fun filterUserProfileMedia( - key: HexKey, - since: Map?, -): List { - if (key.isEmpty()) return emptyList() - - return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - PictureEvent.KIND, - ProfileGalleryEntryEvent.KIND, - VideoVerticalEvent.KIND, - VideoHorizontalEvent.KIND, - ), - authors = listOf(key), - limit = 200, - since = since, - ), - ), +val UserProfileMediaKinds = + listOf( + PictureEvent.KIND, + ProfileGalleryEntryEvent.KIND, + VideoVerticalEvent.KIND, + VideoHorizontalEvent.KIND, ) + +fun filterUserProfileMedia( + user: User, + since: SincePerRelayMap?, +): List { + return user.outboxRelays().map { relay -> + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = UserProfileMediaKinds, + authors = listOf(user.pubkeyHex), + limit = 200, + since = since?.get(relay)?.time, + ), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt index 58564b764f..696dba77b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt @@ -20,37 +20,38 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -fun filterUserProfileMetadata( - keys: Set, - since: Map?, -): List { - if (keys.isEmpty()) return emptyList() - - return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - MetadataEvent.KIND, - AdvertisedRelayListEvent.KIND, - ContactListEvent.KIND, - BadgeProfilesEvent.KIND, - ), - authors = keys.toList(), - since = since, - ), - ), +val UserProfileMetadataKinds = + listOf( + MetadataEvent.KIND, + AdvertisedRelayListEvent.KIND, + ContactListEvent.KIND, + BadgeProfilesEvent.KIND, ) + +fun filterUserProfileMetadata( + users: Map>, + since: SincePerRelayMap?, +): List { + if (users.isEmpty()) return emptyList() + + return users.map { + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = UserProfileMetadataKinds, + authors = it.value.sorted(), + since = since?.get(it.key)?.time, + ), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt index 030e4bd562..43bd81fe9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt @@ -20,13 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent -import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -38,48 +37,52 @@ import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -fun filterUserProfilePosts( - key: HexKey, - since: Map?, -): List { - if (key.isEmpty()) return emptyList() - - return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - TextNoteEvent.KIND, - GenericRepostEvent.KIND, - RepostEvent.KIND, - LongTextNoteEvent.KIND, - PinListEvent.KIND, - PollNoteEvent.KIND, - HighlightEvent.KIND, - WikiNoteEvent.KIND, - ), - authors = listOf(key), - limit = 200, - since = since, - ), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - kinds = - listOf( - TorrentEvent.KIND, - TorrentCommentEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - CommentEvent.KIND, - ), - authors = listOf(key), - limit = 50, - since = since, - ), - ), +val UserProfilePostKinds1 = + listOf( + TextNoteEvent.KIND, + GenericRepostEvent.KIND, + RepostEvent.KIND, + LongTextNoteEvent.KIND, + PinListEvent.KIND, + PollNoteEvent.KIND, + HighlightEvent.KIND, + WikiNoteEvent.KIND, ) + +val UserProfilePostKinds2 = + listOf( + TorrentEvent.KIND, + TorrentCommentEvent.KIND, + InteractiveStoryPrologueEvent.KIND, + CommentEvent.KIND, + ) + +fun filterUserProfilePosts( + user: User, + since: SincePerRelayMap?, +): List { + return user.outboxRelays().map { relay -> + listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = UserProfilePostKinds1, + authors = listOf(user.pubkeyHex), + limit = 200, + since = since?.get(relay)?.time, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = UserProfilePostKinds2, + authors = listOf(user.pubkeyHex), + limit = 50, + since = since?.get(relay)?.time, + ), + ), + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt index d5c91849f2..f9abf7c275 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt @@ -20,29 +20,28 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -fun filterUserProfileZapsReceived( - key: HexKey, - since: Map?, -): List { - if (key.isEmpty()) return emptyList() +val UserProfileZapReceiverKinds = listOf(LnZapEvent.KIND) - return listOf( - TypedFilter( - types = COMMON_FEED_TYPES, +fun filterUserProfileZapsReceived( + user: User, + since: SincePerRelayMap?, +): List { + return user.inboxRelays().map { relay -> + RelayBasedFilter( + relay = relay, filter = - SincePerRelayFilter( - kinds = listOf(LnZapEvent.KIND), - tags = mapOf("p" to listOf(key)), + Filter( + kinds = UserProfileZapReceiverKinds, + tags = mapOf("p" to listOf(user.pubkeyHex)), limit = 200, - since = since, + since = since?.get(relay)?.time, ), - ), - ) + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt index 41737c9830..be10dce706 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to tags, even the same tag class UserProfileQueryState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt index 609c7c4271..dbfd219712 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFollowersFilterSubAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class UserProfileFollowersFilterSubAssembler( client: NostrClient, @@ -31,11 +31,8 @@ class UserProfileFollowersFilterSubAssembler( ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( key: UserProfileQueryState, - since: Map?, - ): List? = - listOfNotNull( - filterUserProfileFollowers(user(key).pubkeyHex, since), - ).flatten() + since: SincePerRelayMap?, + ): List? = filterUserProfileFollowers(user(key), since) override fun user(key: UserProfileQueryState) = key.user } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt index 4c5086d01e..40fadd1a4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMediaFilterSubAssembler.kt @@ -21,10 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import kotlin.collections.flatten +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class UserProfileMediaFilterSubAssembler( client: NostrClient, @@ -32,11 +31,8 @@ class UserProfileMediaFilterSubAssembler( ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( key: UserProfileQueryState, - since: Map?, - ): List? = - listOfNotNull( - filterUserProfileMedia(user(key).pubkeyHex, since), - ).flatten() + since: SincePerRelayMap?, + ): List = filterUserProfileMedia(user(key), since) override fun user(key: UserProfileQueryState) = key.user } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt index 6d6d71cef7..2ed12bc1d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt @@ -21,9 +21,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.utils.mapOfSet import kotlin.collections.flatten class UserProfileMetadataFilterSubAssembler( @@ -32,15 +33,20 @@ class UserProfileMetadataFilterSubAssembler( ) : SingleSubEoseManager(client, allKeys) { override fun updateFilter( keys: List, - since: Map?, - ): List? { - val keys = keys.mapTo(mutableSetOf()) { key -> key.user.pubkeyHex } - - // TODO: Load outbox for each user. + since: SincePerRelayMap?, + ): List? { + val userPerRelay = + mapOfSet { + keys.mapTo(mutableSetOf()) { key -> key.user }.forEach { user -> + user.outboxRelays().forEach { relay -> + add(relay, user.pubkeyHex) + } + } + } return listOfNotNull( - filterUserProfileMetadata(keys, since), - filterUserProfileLists(keys, since), + filterUserProfileMetadata(userPerRelay, since), + filterUserProfileLists(userPerRelay, since), ).flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt index 1d85fb9fa9..a4223f5a69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfilePostsFilterSubAssembler.kt @@ -21,10 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import kotlin.collections.flatten +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class UserProfilePostsFilterSubAssembler( client: NostrClient, @@ -32,11 +31,8 @@ class UserProfilePostsFilterSubAssembler( ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( key: UserProfileQueryState, - since: Map?, - ): List? = - listOfNotNull( - filterUserProfilePosts(user(key).pubkeyHex, since), - ).flatten() + since: SincePerRelayMap?, + ): List = filterUserProfilePosts(user(key), since) override fun user(key: UserProfileQueryState) = key.user } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt index 37b6763bac..6e2a89595b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import kotlin.collections.flatten class UserProfileZapsFilterSubAssembler( @@ -32,10 +32,10 @@ class UserProfileZapsFilterSubAssembler( ) : PerUserEoseManager(client, allKeys) { override fun updateFilter( key: UserProfileQueryState, - since: Map?, - ): List? = + since: SincePerRelayMap?, + ): List? = listOfNotNull( - filterUserProfileZapsReceived(user(key).pubkeyHex, since), + filterUserProfileZapsReceived(user(key), since), ).flatten() override fun user(key: UserProfileQueryState) = key.user diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt index 61eaf4f3bc..633842a57e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt @@ -78,10 +78,8 @@ class UserProfileGalleryFeedFilter( fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( - userHex = account.userProfile().pubkeyHex, - selectedListName = account.settings.defaultStoriesFollowList.value, followLists = account.liveStoriesFollowLists.value, - hiddenUsers = account.flowHiddenUsers.value, + hiddenUsers = account.hiddenUsers.flow.value, ) override fun sort(collection: Set): List = collection.sortedWith(DefaultFeedOrder) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt index d6bdc150d4..fa9c00e4ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt @@ -40,11 +40,11 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness @Composable fun TabFollowedTags( baseUser: User, - account: AccountViewModel, + accountViewModel: AccountViewModel, nav: INav, ) { val items = - remember(baseUser) { + remember(baseUser.latestContactList?.id) { baseUser.latestContactList?.unverifiedFollowTagSet() } @@ -58,7 +58,7 @@ fun TabFollowedTags( itemsIndexed(items) { index, hashtag -> HashtagHeader( tag = hashtag, - account = account, + account = accountViewModel, onClick = { nav.nav(Route.Hashtag(hashtag)) }, ) HorizontalDivider( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt index 1d78d29dc8..42281203d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt @@ -56,10 +56,10 @@ fun RelayFeedView( item, accountViewModel = accountViewModel, onAddRelay = { - nav.nav(Route.EditRelays(item.url)) + nav.nav(Route.EditRelays(item.url.url)) }, onRemoveRelay = { - nav.nav(Route.EditRelays(item.url)) + nav.nav(Route.EditRelays(item.url.url)) }, ) HorizontalDivider( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index 000db6ae45..7c8333adc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -30,8 +30,8 @@ import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.ammolite.relays.BundledUpdate +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ReadWrite -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job @@ -49,7 +49,7 @@ class RelayFeedViewModel : val order = compareByDescending { it.lastEvent } .thenByDescending { it.counter } - .thenBy { it.url } + .thenBy { it.url.url } private val _feedContent = MutableStateFlow>(emptyList()) val feedContent = _feedContent.asStateFlow() @@ -79,14 +79,14 @@ class RelayFeedViewModel : } fun mergeRelays( - relaysBeingUsed: Map, - relays: Map?, + relaysBeingUsed: Map, + relays: Map?, ): List { val userRelaysBeingUsed = relaysBeingUsed.map { it.value } val currentUserRelays = relays?.mapNotNull { - val url = RelayUrlFormatter.normalize(it.key) + val url = it.key if (url !in relaysBeingUsed) { RelayInfo(url, 0, 0) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt index 708c7f10d1..30a3e7ee5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt @@ -29,15 +29,19 @@ import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.uriToRoute import kotlinx.coroutines.CancellationException @Composable -fun NIP19QrCodeScanner(onScan: (Route?) -> Unit) { +fun NIP19QrCodeScanner( + accountViewModel: AccountViewModel, + onScan: (Route?) -> Unit, +) { SimpleQrCodeScanner { try { - onScan(uriToRoute(it)) + onScan(uriToRoute(it, accountViewModel.account)) } catch (e: Throwable) { if (e is CancellationException) throw e Log.e("NIP19 Scanner", "Error parsing $it", e) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt index 0392dc9ad0..8c33d63e09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt @@ -220,7 +220,7 @@ fun ShowQRDialog( } } } else { - NIP19QrCodeScanner { + NIP19QrCodeScanner(accountViewModel) { if (it == null) { presenting = true } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 8783514197..5368a29087 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -57,9 +57,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.renderDMItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3.Kind3RelayListViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3.renderKind3Items -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3.renderKind3ProposalItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.LocalRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.renderLocalItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37.PrivateOutboxRelayListViewModel @@ -77,7 +74,6 @@ import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.grayText -import com.vitorpamplona.ammolite.relays.Constants @Composable fun AllRelayListScreen( @@ -95,10 +91,6 @@ fun MappedAllRelayListView( accountViewModel: AccountViewModel, newNav: INav, ) { - val kind3ViewModel: Kind3RelayListViewModel = viewModel() - val kind3FeedState by kind3ViewModel.relays.collectAsStateWithLifecycle() - val kind3Proposals by kind3ViewModel.proposedRelays.collectAsStateWithLifecycle() - val dmViewModel: DMRelayListViewModel = viewModel() val dmFeedState by dmViewModel.relays.collectAsStateWithLifecycle() @@ -116,7 +108,6 @@ fun MappedAllRelayListView( val localFeedState by localViewModel.relays.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - kind3ViewModel.load(accountViewModel.account) dmViewModel.load(accountViewModel.account) nip65ViewModel.load(accountViewModel.account) searchViewModel.load(accountViewModel.account) @@ -146,7 +137,6 @@ fun MappedAllRelayListView( SaveButton( onPost = { - kind3ViewModel.create() dmViewModel.create() nip65ViewModel.create() searchViewModel.create() @@ -163,7 +153,6 @@ fun MappedAllRelayListView( Spacer(modifier = StdHorzSpacer) CloseButton( onPress = { - kind3ViewModel.clear() dmViewModel.clear() nip65ViewModel.clear() searchViewModel.clear() @@ -252,45 +241,10 @@ fun MappedAllRelayListView( ) } renderLocalItems(localFeedState, localViewModel, accountViewModel, newNav) - - item { - SettingsCategoryWithButton( - stringRes(R.string.kind_3_section), - stringRes(R.string.kind_3_section_description), - SettingsCategorySpacingModifier, - ) { - ResetKind3Relays(kind3ViewModel) - } - } - renderKind3Items(kind3FeedState, kind3ViewModel, accountViewModel, newNav, relayToAdd) - - if (kind3Proposals.isNotEmpty()) { - item { - SettingsCategory( - stringRes(R.string.kind_3_recommended_section), - stringRes(R.string.kind_3_recommended_section_description), - SettingsCategorySpacingModifier, - ) - } - renderKind3ProposalItems(kind3Proposals, kind3ViewModel, accountViewModel, newNav) - } } } } -@Composable -fun ResetKind3Relays(postViewModel: Kind3RelayListViewModel) { - OutlinedButton( - onClick = { - postViewModel.deleteAll() - postViewModel.addAll(Constants.defaultRelays) - postViewModel.loadRelayDocuments() - }, - ) { - Text(stringRes(R.string.default_relays)) - } -} - @Composable fun ResetSearchRelays(postViewModel: SearchRelayListViewModel) { OutlinedButton( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationDialog.kt index 8e71bd5f73..84b450de91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationDialog.kt @@ -74,9 +74,10 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.ammolite.relays.RelayStats -import com.vitorpamplona.quartz.nip01Core.relay.RelayDebugMessage +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayDebugMessage +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import kotlinx.collections.immutable.toImmutableList @@ -85,7 +86,7 @@ import kotlinx.collections.immutable.toImmutableList @Composable fun RelayInformationDialog( onClose: () -> Unit, - relayBriefInfo: RelayBriefInfoCache.RelayBriefInfo, + relay: NormalizedRelayUrl, relayInfo: Nip11RelayInformation, accountViewModel: AccountViewModel, nav: INav, @@ -93,9 +94,9 @@ fun RelayInformationDialog( val newNav = rememberExtendedNav(nav, onClose) val messages = - remember(relayBriefInfo) { + remember(relay) { RelayStats - .get(url = relayBriefInfo.url) + .get(url = relay) .messages .snapshot() .values @@ -119,7 +120,7 @@ fun RelayInformationDialog( TopAppBar( actions = {}, title = { - Text(relayBriefInfo.displayUrl) + Text(relay.displayUrl()) }, navigationIcon = { Row { @@ -148,11 +149,11 @@ fun RelayInformationDialog( ) { Column { RenderRelayIcon( - displayUrl = relayBriefInfo.displayUrl, - iconUrl = relayInfo.icon ?: relayBriefInfo.favIcon, + displayUrl = relay.displayUrl(), + iconUrl = relayInfo.icon, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, - RelayStats.get(url = relayBriefInfo.url).pingInMs, + RelayStats.get(relay).pingInMs, iconModifier = LargeRelayIconModifier, ) } @@ -162,7 +163,7 @@ fun RelayInformationDialog( Column(horizontalAlignment = Alignment.CenterHorizontally) { Title(relayInfo.name?.trim() ?: "") Spacer(modifier = HalfVertSpacer) - SubtitleContent(relayBriefInfo.url) + SubtitleContent(relay.url) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt index e9ec4f33f9..1f13e3d636 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt @@ -21,22 +21,18 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Immutable -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.ammolite.relays.RelayStats -import com.vitorpamplona.quartz.nip01Core.relay.RelayStat -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Immutable data class BasicRelaySetupInfo( - val url: String, + val relay: NormalizedRelayUrl, val relayStat: RelayStat, val paidRelay: Boolean = false, -) { - val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url) -} +) -fun relaySetupInfoBuilder(url: String): BasicRelaySetupInfo { - val normalized = RelayUrlFormatter.normalize(url) +fun relaySetupInfoBuilder(normalized: NormalizedRelayUrl): BasicRelaySetupInfo { return BasicRelaySetupInfo( normalized, RelayStats.get(normalized), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt index 910d0dd101..6e54462ba0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt @@ -42,6 +42,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChatMaxWidth +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @OptIn(ExperimentalFoundationApi::class) @Composable @@ -60,7 +61,7 @@ fun BasicRelaySetupInfoClickableRow( .combinedClickable( onClick = onClick, onLongClick = { - clipboardManager.setText(AnnotatedString(item.briefInfo.url)) + clipboardManager.setText(AnnotatedString(item.relay.url)) }, ), ) { @@ -68,11 +69,11 @@ fun BasicRelaySetupInfoClickableRow( verticalAlignment = Alignment.CenterVertically, modifier = HalfVertPadding, ) { - val iconUrlFromRelayInfoDoc by loadRelayInfo(item.url, accountViewModel) + val iconUrlFromRelayInfoDoc by loadRelayInfo(item.relay, accountViewModel) RenderRelayIcon( - item.briefInfo.displayUrl, - iconUrlFromRelayInfoDoc?.icon ?: item.briefInfo.favIcon, + item.relay.displayUrl(), + iconUrlFromRelayInfoDoc?.icon, loadProfilePicture, loadRobohash, item.relayStat.pingInMs, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 9739139789..465dcf5005 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -34,7 +34,6 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache @Composable fun BasicRelaySetupInfoDialog( @@ -50,7 +49,7 @@ fun BasicRelaySetupInfoDialog( RelayInformationDialog( onClose = { relayInfo = null }, relayInfo = it.relayInfo, - relayBriefInfo = it.relayBriefInfo, + relay = it.relay, accountViewModel = accountViewModel, nav = nav, ) @@ -64,18 +63,18 @@ fun BasicRelaySetupInfoDialog( accountViewModel = accountViewModel, onClick = { accountViewModel.retrieveRelayDocument( - item.url, + relay = item.relay, onInfo = { - relayInfo = RelayInfoDialog(RelayBriefInfoCache.RelayBriefInfo(item.url), it) + relayInfo = RelayInfoDialog(item.relay, it) }, - onError = { url, errorCode, exceptionMessage -> + onError = { relay, errorCode, exceptionMessage -> val msg = when (errorCode) { Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> stringRes( context, R.string.relay_information_document_error_assemble_url, - url, + relay.url, exceptionMessage, ) @@ -83,7 +82,7 @@ fun BasicRelaySetupInfoDialog( stringRes( context, R.string.relay_information_document_error_assemble_url, - url, + relay.url, exceptionMessage, ) @@ -91,7 +90,7 @@ fun BasicRelaySetupInfoDialog( stringRes( context, R.string.relay_information_document_error_assemble_url, - url, + relay.url, exceptionMessage, ) @@ -99,7 +98,7 @@ fun BasicRelaySetupInfoDialog( stringRes( context, R.string.relay_information_document_error_assemble_url, - url, + relay.url, exceptionMessage, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index 067beb38d4..d53a68e268 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.replace +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -46,14 +47,14 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { loadRelayDocuments() } - abstract fun getRelayList(): List? + abstract fun getRelayList(): List? - abstract fun saveRelayList(urlList: List) + abstract fun saveRelayList(urlList: List) fun create() { if (hasModified) { viewModelScope.launch(Dispatchers.IO) { - saveRelayList(_relays.value.map { it.url }) + saveRelayList(_relays.value.map { it.relay }) clear() } } @@ -63,8 +64,10 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { viewModelScope.launch(Dispatchers.IO) { _relays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( - dirtyUrl = item.url, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForDirty(item.url)) }, + relay = item.relay, + okHttpClient = { + Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) + }, onInfo = { togglePaidRelay(item, it.limitation?.payment_required ?: false) }, @@ -81,14 +84,14 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { relayList .map { relaySetupInfoBuilder(it) } - .distinctBy { it.url } + .distinctBy { it.relay } .sortedBy { it.relayStat.receivedBytes } .reversed() } } fun addRelay(relay: BasicRelaySetupInfo) { - if (relays.value.any { it.url == relay.url }) return + if (relays.value.any { it.relay == relay.relay }) return _relays.update { it.plus(relay) } hasModified = true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt index 6915ce6b41..183751a329 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.WarningColor import com.vitorpamplona.amethyst.ui.theme.allGoodColor +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @OptIn(ExperimentalFoundationApi::class) @Composable @@ -56,12 +57,12 @@ fun RelayNameAndRemoveButton( Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { Text( - text = item.briefInfo.displayUrl, + text = item.relay.displayUrl(), modifier = Modifier.combinedClickable( onClick = onClick, onLongClick = { - clipboardManager.setText(AnnotatedString(item.briefInfo.url)) + clipboardManager.setText(AnnotatedString(item.relay.url)) }, ), maxLines = 1, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt index f21bc2e095..9eadc8c941 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayUrlEditField.kt @@ -47,6 +47,8 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer @Preview @Composable @@ -57,7 +59,7 @@ fun RelayUrlEditFieldPreview() { } @Composable -fun RelayUrlEditField(onNewRelay: (String) -> Unit) { +fun RelayUrlEditField(onNewRelay: (NormalizedRelayUrl) -> Unit) { var url by remember { mutableStateOf("") } Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Size10dp)) { @@ -85,8 +87,11 @@ fun RelayUrlEditField(onNewRelay: (String) -> Unit) { KeyboardActions( onGo = { if (url.isNotBlank() && url != "/") { - onNewRelay(url) - url = "" + val relay = RelayUrlNormalizer.normalizeOrNull(url) + if (relay != null) { + onNewRelay(relay) + url = "" + } } }, ), @@ -95,8 +100,11 @@ fun RelayUrlEditField(onNewRelay: (String) -> Unit) { Button( onClick = { if (url.isNotBlank() && url != "/") { - onNewRelay(url) - url = "" + val relay = RelayUrlNormalizer.normalizeOrNull(url) + if (relay != null) { + onNewRelay(relay) + url = "" + } } }, shape = ButtonBorder, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt index 6abd8e54be..50c4fbf22f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt @@ -64,7 +64,7 @@ fun LazyListScope.renderDMItems( accountViewModel: AccountViewModel, nav: INav, ) { - itemsIndexed(feedState, key = { _, item -> "DM" + item.url }) { index, item -> + itemsIndexed(feedState, key = { _, item -> "DM" + item.relay }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteRelay(item) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt index dc47689185..581ed87643 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt @@ -21,11 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class DMRelayListViewModel : BasicRelaySetupInfoModel() { - override fun getRelayList(): List? = account.getDMRelayList()?.relays() + override fun getRelayList(): List? = account.dmRelayList.getDMRelayList()?.relays() - override fun saveRelayList(urlList: List) { + override fun saveRelayList(urlList: List) { account.saveDMRelayList(urlList) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListView.kt deleted file mode 100644 index 8ad32dacbf..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListView.kt +++ /dev/null @@ -1,824 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3 - -import android.widget.Toast -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Cancel -import androidx.compose.material.icons.filled.DeleteSweep -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.Groups -import androidx.compose.material.icons.filled.Paid -import androidx.compose.material.icons.filled.Public -import androidx.compose.material.icons.filled.SyncProblem -import androidx.compose.material.icons.filled.Upload -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.service.Nip11CachedRetriever -import com.vitorpamplona.amethyst.service.Nip11Retriever -import com.vitorpamplona.amethyst.service.countToHumanReadable -import com.vitorpamplona.amethyst.service.countToHumanReadableBytes -import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon -import com.vitorpamplona.amethyst.ui.painterRes -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations.Kind3RelayProposalSetupInfo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations.Kind3RelaySetupInfoProposalDialog -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.ButtonBorder -import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding -import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding -import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier -import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChatMaxWidth -import com.vitorpamplona.amethyst.ui.theme.Size30Modifier -import com.vitorpamplona.amethyst.ui.theme.Size35dp -import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import com.vitorpamplona.amethyst.ui.theme.WarningColor -import com.vitorpamplona.amethyst.ui.theme.allGoodColor -import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.theme.warningColor -import com.vitorpamplona.ammolite.relays.Constants.activeTypesGlobalChats -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache -import com.vitorpamplona.ammolite.relays.RelayStats -import com.vitorpamplona.quartz.nip01Core.relay.RelayStat -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter -import kotlinx.coroutines.launch - -@Composable -fun Kind3RelayListView( - feedState: List, - postViewModel: Kind3RelayListViewModel, - accountViewModel: AccountViewModel, - onClose: () -> Unit, - nav: INav, - relayToAdd: String, -) { - val newNav = rememberExtendedNav(nav, onClose) - - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - LazyColumn(contentPadding = FeedPadding) { - renderKind3Items(feedState, postViewModel, accountViewModel, newNav, relayToAdd) - } - } -} - -fun LazyListScope.renderKind3Items( - feedState: List, - postViewModel: Kind3RelayListViewModel, - accountViewModel: AccountViewModel, - nav: INav, - relayToAdd: String, -) { - itemsIndexed(feedState, key = { _, item -> "kind3" + item.url }) { index, item -> - LoadRelayInfo( - item, - onToggleDownload = { postViewModel.toggleDownload(it) }, - onToggleUpload = { postViewModel.toggleUpload(it) }, - onToggleFollows = { postViewModel.toggleFollows(it) }, - onTogglePrivateDMs = { postViewModel.toggleMessages(it) }, - onTogglePublicChats = { postViewModel.togglePublicChats(it) }, - onToggleGlobal = { postViewModel.toggleGlobal(it) }, - onToggleSearch = { postViewModel.toggleSearch(it) }, - onDelete = { postViewModel.deleteRelay(it) }, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - item { - Spacer(modifier = StdVertSpacer) - Kind3RelayEditBox(relayToAdd) { postViewModel.addRelay(it) } - } -} - -fun LazyListScope.renderKind3ProposalItems( - feedState: List, - postViewModel: Kind3RelayListViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - itemsIndexed(feedState, key = { _, item -> "kind3proposal" + item.url }) { index, item -> - Kind3RelaySetupInfoProposalDialog( - item = item, - onAdd = { - postViewModel.addRelay(item) - }, - accountViewModel = accountViewModel, - nav = nav, - ) - HorizontalDivider( - thickness = DividerThickness, - ) - } -} - -@Preview -@Composable -fun ServerConfigPreview() { - ClickableRelayItem( - loadProfilePicture = true, - loadRobohash = false, - item = - Kind3BasicRelaySetupInfo( - url = "nostr.mom", - read = true, - write = true, - relayStat = - RelayStat( - errorCounter = 23, - receivedBytes = 10000, - sentBytes = 10000000, - spamCounter = 10, - ), - feedTypes = activeTypesGlobalChats, - paidRelay = true, - ), - onDelete = {}, - onToggleDownload = {}, - onToggleUpload = {}, - onToggleFollows = {}, - onTogglePrivateDMs = {}, - onTogglePublicChats = {}, - onToggleGlobal = {}, - onToggleSearch = {}, - onClick = {}, - ) -} - -@Composable -fun LoadRelayInfo( - item: Kind3BasicRelaySetupInfo, - onToggleDownload: (Kind3BasicRelaySetupInfo) -> Unit, - onToggleUpload: (Kind3BasicRelaySetupInfo) -> Unit, - onToggleFollows: (Kind3BasicRelaySetupInfo) -> Unit, - onTogglePrivateDMs: (Kind3BasicRelaySetupInfo) -> Unit, - onTogglePublicChats: (Kind3BasicRelaySetupInfo) -> Unit, - onToggleGlobal: (Kind3BasicRelaySetupInfo) -> Unit, - onToggleSearch: (Kind3BasicRelaySetupInfo) -> Unit, - onDelete: (Kind3BasicRelaySetupInfo) -> Unit, - accountViewModel: AccountViewModel, - nav: INav, -) { - var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) } - val context = LocalContext.current - - relayInfo?.let { - RelayInformationDialog( - onClose = { relayInfo = null }, - relayInfo = it.relayInfo, - relayBriefInfo = it.relayBriefInfo, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - ClickableRelayItem( - item = item, - loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, - onToggleDownload = onToggleDownload, - onToggleUpload = onToggleUpload, - onToggleFollows = onToggleFollows, - onTogglePrivateDMs = onTogglePrivateDMs, - onTogglePublicChats = onTogglePublicChats, - onToggleGlobal = onToggleGlobal, - onToggleSearch = onToggleSearch, - onDelete = onDelete, - onClick = { - accountViewModel.retrieveRelayDocument( - item.url, - onInfo = { - relayInfo = RelayInfoDialog(RelayBriefInfoCache.RelayBriefInfo(item.url), it) - }, - onError = { url, errorCode, exceptionMessage -> - val msg = - when (errorCode) { - Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) - - Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) - - Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) - - Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) - } - - accountViewModel.toastManager.toast( - stringRes(context, R.string.unable_to_download_relay_document), - msg, - ) - }, - ) - }, - ) -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ClickableRelayItem( - item: Kind3BasicRelaySetupInfo, - loadProfilePicture: Boolean, - loadRobohash: Boolean, - onToggleDownload: (Kind3BasicRelaySetupInfo) -> Unit, - onToggleUpload: (Kind3BasicRelaySetupInfo) -> Unit, - onToggleFollows: (Kind3BasicRelaySetupInfo) -> Unit, - onTogglePrivateDMs: (Kind3BasicRelaySetupInfo) -> Unit, - onTogglePublicChats: (Kind3BasicRelaySetupInfo) -> Unit, - onToggleGlobal: (Kind3BasicRelaySetupInfo) -> Unit, - onToggleSearch: (Kind3BasicRelaySetupInfo) -> Unit, - onDelete: (Kind3BasicRelaySetupInfo) -> Unit, - onClick: () -> Unit, -) { - val clipboardManager = LocalClipboardManager.current - Column( - Modifier - .fillMaxWidth() - .combinedClickable( - onClick = onClick, - onLongClick = { - clipboardManager.setText(AnnotatedString(item.briefInfo.url)) - }, - ), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(vertical = 5.dp), - ) { - val relayInfo = Nip11CachedRetriever.getFromCache(item.url) - - RenderRelayIcon( - item.briefInfo.displayUrl, - relayInfo?.icon ?: item.briefInfo.favIcon, - loadProfilePicture, - loadRobohash, - item.relayStat.pingInMs, - LargeRelayIconModifier, - ) - - Spacer(modifier = HalfHorzPadding) - - Column(Modifier.weight(1f)) { - FirstLine(item, onClick, onDelete, ReactionRowHeightChatMaxWidth) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = ReactionRowHeightChatMaxWidth, - ) { - ActiveToggles( - item = item, - onToggleFollows = onToggleFollows, - onTogglePrivateDMs = onTogglePrivateDMs, - onTogglePublicChats = onTogglePublicChats, - onToggleGlobal = onToggleGlobal, - ) - } - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = ReactionRowHeightChatMaxWidth, - ) { - StatusRow( - item = item, - onToggleDownload = onToggleDownload, - onToggleUpload = onToggleUpload, - modifier = HalfStartPadding.weight(1f), - ) - } - } - } - - HorizontalDivider(thickness = DividerThickness) - } -} - -@Composable -@OptIn(ExperimentalFoundationApi::class) -private fun StatusRow( - item: Kind3BasicRelaySetupInfo, - onToggleDownload: (Kind3BasicRelaySetupInfo) -> Unit, - onToggleUpload: (Kind3BasicRelaySetupInfo) -> Unit, - modifier: Modifier, -) { - val scope = rememberCoroutineScope() - val context = LocalContext.current - - Icon( - imageVector = Icons.Default.Download, - contentDescription = stringRes(R.string.read_from_relay), - modifier = - Modifier - .size(15.dp) - .combinedClickable( - onClick = { onToggleDownload(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - stringRes(context, R.string.read_from_relay), - Toast.LENGTH_SHORT, - ).show() - } - }, - ), - tint = - if (item.read) { - MaterialTheme.colorScheme.allGoodColor - } else { - MaterialTheme.colorScheme.onSurface.copy( - alpha = 0.32f, - ) - }, - ) - - Text( - text = countToHumanReadableBytes(item.relayStat.receivedBytes), - maxLines = 1, - fontSize = 12.sp, - modifier = modifier, - color = MaterialTheme.colorScheme.placeholderText, - ) - - Icon( - imageVector = Icons.Default.Upload, - stringRes(R.string.write_to_relay), - modifier = - Modifier - .size(15.dp) - .combinedClickable( - onClick = { onToggleUpload(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - stringRes(context, R.string.write_to_relay), - Toast.LENGTH_SHORT, - ).show() - } - }, - ), - tint = - if (item.write) { - MaterialTheme.colorScheme.allGoodColor - } else { - MaterialTheme.colorScheme.onSurface.copy( - alpha = 0.32f, - ) - }, - ) - - Text( - text = countToHumanReadableBytes(item.relayStat.sentBytes), - maxLines = 1, - fontSize = 12.sp, - modifier = modifier, - color = MaterialTheme.colorScheme.placeholderText, - ) - - Icon( - imageVector = Icons.Default.SyncProblem, - stringRes(R.string.errors), - modifier = - Modifier - .size(15.dp) - .combinedClickable( - onClick = {}, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - stringRes(context, R.string.errors), - Toast.LENGTH_SHORT, - ).show() - } - }, - ), - tint = - if (item.relayStat.errorCounter > 0) { - MaterialTheme.colorScheme.warningColor - } else { - MaterialTheme.colorScheme.allGoodColor - }, - ) - - Text( - text = countToHumanReadable(item.relayStat.errorCounter, "errors"), - maxLines = 1, - fontSize = 12.sp, - modifier = modifier, - color = MaterialTheme.colorScheme.placeholderText, - ) - - Icon( - imageVector = Icons.Default.DeleteSweep, - stringRes(R.string.spam), - modifier = - Modifier - .size(15.dp) - .combinedClickable( - onClick = {}, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - stringRes(context, R.string.spam), - Toast.LENGTH_SHORT, - ).show() - } - }, - ), - tint = - if (item.relayStat.spamCounter > 0) { - MaterialTheme.colorScheme.warningColor - } else { - MaterialTheme.colorScheme.allGoodColor - }, - ) - - Text( - text = countToHumanReadable(item.relayStat.spamCounter, "spam"), - maxLines = 1, - fontSize = 12.sp, - modifier = modifier, - color = MaterialTheme.colorScheme.placeholderText, - ) -} - -@Composable -@OptIn(ExperimentalFoundationApi::class) -private fun ActiveToggles( - item: Kind3BasicRelaySetupInfo, - onToggleFollows: (Kind3BasicRelaySetupInfo) -> Unit, - onTogglePrivateDMs: (Kind3BasicRelaySetupInfo) -> Unit, - onTogglePublicChats: (Kind3BasicRelaySetupInfo) -> Unit, - onToggleGlobal: (Kind3BasicRelaySetupInfo) -> Unit, -) { - val scope = rememberCoroutineScope() - val context = LocalContext.current - - Text( - text = stringRes(id = R.string.active_for), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.placeholderText, - modifier = Modifier.padding(start = 2.dp, end = 5.dp), - fontSize = 14.sp, - ) - - IconButton( - modifier = Size30Modifier, - onClick = { onToggleFollows(item) }, - ) { - Icon( - painterRes(R.drawable.ic_home), - stringRes(R.string.home_feed), - modifier = - Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleFollows(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - stringRes(context, R.string.home_feed), - Toast.LENGTH_SHORT, - ).show() - } - }, - ), - tint = - if (item.feedTypes.contains(FeedType.FOLLOWS)) { - MaterialTheme.colorScheme.allGoodColor - } else { - MaterialTheme.colorScheme.onSurface.copy( - alpha = 0.32f, - ) - }, - ) - } - IconButton( - modifier = Size30Modifier, - onClick = { onTogglePrivateDMs(item) }, - ) { - Icon( - painterRes(R.drawable.ic_dm), - stringRes(R.string.private_message_feed), - modifier = - Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onTogglePrivateDMs(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - stringRes(context, R.string.private_message_feed), - Toast.LENGTH_SHORT, - ).show() - } - }, - ), - tint = - if (item.feedTypes.contains(FeedType.PRIVATE_DMS)) { - MaterialTheme.colorScheme.allGoodColor - } else { - MaterialTheme.colorScheme.onSurface.copy( - alpha = 0.32f, - ) - }, - ) - } - IconButton( - modifier = Size30Modifier, - onClick = { onTogglePublicChats(item) }, - ) { - Icon( - imageVector = Icons.Default.Groups, - contentDescription = stringRes(R.string.public_chat_feed), - modifier = - Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onTogglePublicChats(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - stringRes(context, R.string.public_chat_feed), - Toast.LENGTH_SHORT, - ).show() - } - }, - ), - tint = - if (item.feedTypes.contains(FeedType.PUBLIC_CHATS)) { - MaterialTheme.colorScheme.allGoodColor - } else { - MaterialTheme.colorScheme.onSurface.copy( - alpha = 0.32f, - ) - }, - ) - } - IconButton( - modifier = Size30Modifier, - onClick = { onToggleGlobal(item) }, - ) { - Icon( - imageVector = Icons.Default.Public, - stringRes(R.string.global_feed), - modifier = - Modifier - .padding(horizontal = 5.dp) - .size(15.dp) - .combinedClickable( - onClick = { onToggleGlobal(item) }, - onLongClick = { - scope.launch { - Toast - .makeText( - context, - stringRes(context, R.string.global_feed), - Toast.LENGTH_SHORT, - ).show() - } - }, - ), - tint = - if (item.feedTypes.contains(FeedType.GLOBAL)) { - MaterialTheme.colorScheme.allGoodColor - } else { - MaterialTheme.colorScheme.onSurface.copy( - alpha = 0.32f, - ) - }, - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun FirstLine( - item: Kind3BasicRelaySetupInfo, - onClick: () -> Unit, - onDelete: (Kind3BasicRelaySetupInfo) -> Unit, - modifier: Modifier, -) { - val clipboardManager = LocalClipboardManager.current - Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { - Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { - Text( - text = item.briefInfo.displayUrl, - modifier = - Modifier.combinedClickable( - onClick = onClick, - onLongClick = { - clipboardManager.setText(AnnotatedString(item.briefInfo.url)) - }, - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - if (item.paidRelay) { - Icon( - imageVector = Icons.Default.Paid, - null, - modifier = Modifier.padding(start = 5.dp, top = 1.dp).size(14.dp), - tint = MaterialTheme.colorScheme.allGoodColor, - ) - } - } - - IconButton( - modifier = Modifier.size(30.dp), - onClick = { onDelete(item) }, - ) { - Icon( - imageVector = Icons.Default.Cancel, - contentDescription = stringRes(id = R.string.remove), - modifier = Modifier.padding(start = 10.dp).size(15.dp), - tint = WarningColor, - ) - } - } -} - -@Composable -fun Kind3RelayEditBox( - relayToAdd: String, - onNewRelay: (Kind3BasicRelaySetupInfo) -> Unit, -) { - var url by remember { mutableStateOf(relayToAdd) } - var read by remember { mutableStateOf(true) } - var write by remember { mutableStateOf(true) } - - Row(verticalAlignment = Alignment.CenterVertically) { - OutlinedTextField( - label = { Text(text = stringRes(R.string.add_a_relay)) }, - modifier = Modifier.weight(1f), - value = url, - onValueChange = { url = it }, - placeholder = { - Text( - text = "server.com", - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - ) - }, - singleLine = true, - ) - - IconButton(onClick = { read = !read }) { - Icon( - imageVector = Icons.Default.Download, - contentDescription = stringRes(id = R.string.read_from_relay), - modifier = Modifier.size(Size35dp).padding(horizontal = 5.dp), - tint = - if (read) { - MaterialTheme.colorScheme.allGoodColor - } else { - MaterialTheme.colorScheme.placeholderText - }, - ) - } - - IconButton(onClick = { write = !write }) { - Icon( - imageVector = Icons.Default.Upload, - contentDescription = stringRes(id = R.string.write_to_relay), - modifier = Modifier.size(Size35dp).padding(horizontal = 5.dp), - tint = - if (write) { - MaterialTheme.colorScheme.allGoodColor - } else { - MaterialTheme.colorScheme.placeholderText - }, - ) - } - - Button( - onClick = { - if (url.isNotBlank() && url != "/") { - val normalized = RelayUrlFormatter.normalize(url) - onNewRelay( - Kind3BasicRelaySetupInfo( - url = normalized, - read = read, - write = write, - feedTypes = activeTypesGlobalChats, - relayStat = RelayStats.get(normalized), - ), - ) - url = "" - write = true - read = true - } - }, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = - if (url.isNotBlank()) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.placeholderText - }, - ), - ) { - Text(text = stringRes(id = R.string.add), color = Color.White) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt deleted file mode 100644 index 70bf368a99..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/kind3/Kind3RelayListViewModel.kt +++ /dev/null @@ -1,293 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3 - -import androidx.compose.runtime.Stable -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.service.Nip11CachedRetriever -import com.vitorpamplona.amethyst.service.replace -import com.vitorpamplona.amethyst.service.togglePresenceInSet -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations.Kind3RelayProposalSetupInfo -import com.vitorpamplona.ammolite.relays.Constants -import com.vitorpamplona.ammolite.relays.Constants.activeTypesGlobalChats -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.RelaySetupInfo -import com.vitorpamplona.ammolite.relays.RelayStats -import com.vitorpamplona.quartz.nip65RelayList.RelayListRecommendationProcessor -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter -import kotlinx.collections.immutable.toImmutableSet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -@Stable -class Kind3RelayListViewModel : ViewModel() { - private lateinit var account: Account - - private val _relays = MutableStateFlow>(emptyList()) - val relays = _relays.asStateFlow() - - private val _proposedRelays = MutableStateFlow>(emptyList()) - val proposedRelays = _proposedRelays.asStateFlow() - - var hasModified = false - - fun load(account: Account) { - this.account = account - clear() - loadRelayDocuments() - } - - fun create() { - if (hasModified) { - viewModelScope.launch(Dispatchers.IO) { - account.saveKind3RelayList( - relays.value.map { - RelaySetupInfo( - it.url, - it.read, - it.write, - it.feedTypes, - ) - }, - ) - clear() - } - } - } - - fun loadRelayDocuments() { - viewModelScope.launch(Dispatchers.IO) { - _relays.value.forEach { item -> - Nip11CachedRetriever.loadRelayInfo( - dirtyUrl = item.url, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForDirty(item.url)) }, - onInfo = { - togglePaidRelay(item, it.limitation?.payment_required ?: false) - }, - onError = { url, errorCode, exceptionMessage -> }, - ) - } - } - } - - fun clear() { - hasModified = false - _relays.update { - var relayFile = account.userProfile().latestContactList?.relays() - - if (relayFile != null) { - relayFile - .map { - val localInfoFeedTypes = - account.settings.localRelays - .filter { localRelay -> localRelay.url == it.key } - .firstOrNull() - ?.feedTypes - ?: Constants.defaultRelays - .filter { defaultRelay -> defaultRelay.url == it.key } - .firstOrNull() - ?.feedTypes - ?: activeTypesGlobalChats.toImmutableSet() - - Kind3BasicRelaySetupInfo( - url = RelayUrlFormatter.normalize(it.key), - read = it.value.read, - write = it.value.write, - feedTypes = localInfoFeedTypes, - relayStat = RelayStats.get(it.key), - ) - }.distinctBy { it.url } - .sortedBy { it.relayStat.receivedBytes } - .reversed() - } else { - account.settings.localRelays - .map { - Kind3BasicRelaySetupInfo( - url = RelayUrlFormatter.normalize(it.url), - read = it.read, - write = it.write, - feedTypes = it.feedTypes, - relayStat = RelayStats.get(it.url), - ) - }.distinctBy { it.url } - .sortedBy { it.relayStat.receivedBytes } - .reversed() - } - } - - refreshProposals() - } - - private fun refreshProposals() { - _proposedRelays.update { - val proposed = - RelayListRecommendationProcessor - .reliableRelaySetFor( - account.liveKind3Follows.value.authors.mapNotNull { - account.getNIP65RelayList(it) - }, - relayUrlsToIgnore = - _relays.value.mapNotNullTo(HashSet()) { - if (it.read && FeedType.FOLLOWS in it.feedTypes) { - it.url - } else { - null - } - }, - hasOnionConnection = false, - ).sortedByDescending { it.users.size } - - proposed.mapNotNull { - if (it.requiredToNotMissEvents) { - Kind3RelayProposalSetupInfo( - url = RelayUrlFormatter.normalize(it.url), - read = true, - write = false, - feedTypes = setOf(FeedType.FOLLOWS), - relayStat = RelayStats.get(it.url), - users = it.users.sorted(), - ) - } else { - null - } - } - } - } - - fun addAll(defaultRelays: Array) { - hasModified = true - - _relays.update { - defaultRelays - .map { - Kind3BasicRelaySetupInfo( - url = RelayUrlFormatter.normalize(it.url), - read = it.read, - write = it.write, - feedTypes = it.feedTypes, - relayStat = RelayStats.get(it.url), - ) - }.distinctBy { it.url } - .sortedBy { it.relayStat.receivedBytes } - .reversed() - } - } - - fun addRelay(relay: Kind3BasicRelaySetupInfo) { - if (relays.value.any { it.url == relay.url }) return - - _relays.update { it.plus(relay) } - - refreshProposals() - - hasModified = true - } - - fun addRelay(relay: Kind3RelayProposalSetupInfo) { - if (relays.value.any { it.url == relay.url }) return - - _relays.update { - it.plus( - Kind3BasicRelaySetupInfo( - relay.url, - relay.read, - relay.write, - relay.feedTypes, - relay.relayStat, - relay.paidRelay, - ), - ) - } - - refreshProposals() - - hasModified = true - } - - fun deleteRelay(relay: Kind3BasicRelaySetupInfo) { - _relays.update { it.minus(relay) } - - refreshProposals() - - hasModified = true - } - - fun deleteAll() { - _relays.update { relays -> emptyList() } - - refreshProposals() - - hasModified = true - } - - fun toggleDownload(relay: Kind3BasicRelaySetupInfo) { - _relays.update { it.replace(relay, relay.copy(read = !relay.read)) } - hasModified = true - } - - fun toggleUpload(relay: Kind3BasicRelaySetupInfo) { - _relays.update { it.replace(relay, relay.copy(write = !relay.write)) } - hasModified = true - } - - fun toggleFollows(relay: Kind3BasicRelaySetupInfo) { - val newTypes = relay.feedTypes.togglePresenceInSet(FeedType.FOLLOWS) - _relays.update { it.replace(relay, relay.copy(feedTypes = newTypes)) } - hasModified = true - } - - fun toggleMessages(relay: Kind3BasicRelaySetupInfo) { - val newTypes = relay.feedTypes.togglePresenceInSet(FeedType.PRIVATE_DMS) - _relays.update { it.replace(relay, relay.copy(feedTypes = newTypes)) } - hasModified = true - } - - fun togglePublicChats(relay: Kind3BasicRelaySetupInfo) { - val newTypes = relay.feedTypes.togglePresenceInSet(FeedType.PUBLIC_CHATS) - _relays.update { it.replace(relay, relay.copy(feedTypes = newTypes)) } - hasModified = true - } - - fun toggleGlobal(relay: Kind3BasicRelaySetupInfo) { - val newTypes = relay.feedTypes.togglePresenceInSet(FeedType.GLOBAL) - _relays.update { it.replace(relay, relay.copy(feedTypes = newTypes)) } - hasModified = true - } - - fun toggleSearch(relay: Kind3BasicRelaySetupInfo) { - val newTypes = relay.feedTypes.togglePresenceInSet(FeedType.SEARCH) - _relays.update { it.replace(relay, relay.copy(feedTypes = newTypes)) } - hasModified = true - } - - fun togglePaidRelay( - relay: Kind3BasicRelaySetupInfo, - paid: Boolean, - ) { - _relays.update { it.replace(relay, relay.copy(paidRelay = paid)) } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt index d11d524a73..e50b13994e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt @@ -64,7 +64,7 @@ fun LazyListScope.renderLocalItems( accountViewModel: AccountViewModel, nav: INav, ) { - itemsIndexed(feedState, key = { _, item -> "Local" + item.url }) { index, item -> + itemsIndexed(feedState, key = { _, item -> "Local" + item.relay }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteRelay(item) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt index d674a2cb74..615668f819 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt @@ -21,11 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class LocalRelayListViewModel : BasicRelaySetupInfoModel() { - override fun getRelayList(): List = account.settings.localRelayServers.toList() + override fun getRelayList(): List = account.localRelayList.flow.value.toList() - override fun saveRelayList(urlList: List) { - account.settings.updateLocalRelayServers(urlList.toSet()) + override fun saveRelayList(urlList: List) { + account.localRelayList.saveRelayList(urlList) {} } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt index a159087aee..ed4be6a3e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt @@ -64,7 +64,7 @@ fun LazyListScope.renderPrivateOutboxItems( accountViewModel: AccountViewModel, nav: INav, ) { - itemsIndexed(feedState, key = { _, item -> "Outbox" + item.url }) { index, item -> + itemsIndexed(feedState, key = { _, item -> "Outbox" + item.relay }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteRelay(item) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt index e836022181..c6067fcdbc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt @@ -21,11 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37 import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class PrivateOutboxRelayListViewModel : BasicRelaySetupInfoModel() { - override fun getRelayList(): List? = account.getPrivateOutboxRelayList()?.relays() + override fun getRelayList(): List? = account.privateStorageRelayList.getPrivateOutboxRelayList()?.relays() - override fun saveRelayList(urlList: List) { + override fun saveRelayList(urlList: List) { account.savePrivateOutboxRelayList(urlList) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt index 5d3c494625..829b36153e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt @@ -67,7 +67,7 @@ fun LazyListScope.renderNip65HomeItems( accountViewModel: AccountViewModel, nav: INav, ) { - itemsIndexed(feedState, key = { _, item -> "Nip65Home" + item.url }) { index, item -> + itemsIndexed(feedState, key = { _, item -> "Nip65Home" + item.relay }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteHomeRelay(item) }, @@ -88,7 +88,7 @@ fun LazyListScope.renderNip65NotifItems( accountViewModel: AccountViewModel, nav: INav, ) { - itemsIndexed(feedState, key = { _, item -> "Nip65Notif" + item.url }) { index, item -> + itemsIndexed(feedState, key = { _, item -> "Nip65Notif" + item.relay }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteNotifRelay(item) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index 9fcde2682e..fee19cddec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -29,7 +29,8 @@ import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo +import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -57,8 +58,8 @@ class Nip65RelayListViewModel : ViewModel() { fun create() { if (hasModified) { viewModelScope.launch(Dispatchers.IO) { - val writes = _homeRelays.value.map { it.url }.toSet() - val reads = _notificationRelays.value.map { it.url }.toSet() + val writes = _homeRelays.value.map { it.relay }.toSet() + val reads = _notificationRelays.value.map { it.relay }.toSet() val urls = writes.union(reads) @@ -66,13 +67,14 @@ class Nip65RelayListViewModel : ViewModel() { urls.map { val type = if (writes.contains(it) && reads.contains(it)) { - AdvertisedRelayListEvent.AdvertisedRelayType.BOTH + AdvertisedRelayType.BOTH } else if (writes.contains(it)) { - AdvertisedRelayListEvent.AdvertisedRelayType.WRITE + AdvertisedRelayType.WRITE } else { - AdvertisedRelayListEvent.AdvertisedRelayType.READ + AdvertisedRelayType.READ } - AdvertisedRelayListEvent.AdvertisedRelayInfo(it, type) + + AdvertisedRelayInfo(it, type) }, ) clear() @@ -84,8 +86,8 @@ class Nip65RelayListViewModel : ViewModel() { viewModelScope.launch(Dispatchers.IO) { _homeRelays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( - dirtyUrl = item.url, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForDirty(item.url)) }, + relay = item.relay, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) }, onInfo = { toggleHomePaidRelay(item, it.limitation?.payment_required ?: false) }, @@ -95,8 +97,8 @@ class Nip65RelayListViewModel : ViewModel() { _notificationRelays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( - dirtyUrl = item.url, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForDirty(item.url)) }, + relay = item.relay, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) }, onInfo = { toggleNotifPaidRelay(item, it.limitation?.payment_required ?: false) }, @@ -109,28 +111,28 @@ class Nip65RelayListViewModel : ViewModel() { fun clear() { hasModified = false _homeRelays.update { - val relayList = account.getNIP65RelayList()?.writeRelays() ?: emptyList() + val relayList = account.nip65RelayList.getNIP65RelayList()?.writeRelaysNorm() ?: emptyList() relayList .map { relaySetupInfoBuilder(it) } - .distinctBy { it.url } + .distinctBy { it.relay } .sortedBy { it.relayStat.receivedBytes } .reversed() } _notificationRelays.update { - val relayList = account.getNIP65RelayList()?.readRelays() ?: emptyList() + val relayList = account.nip65RelayList.getNIP65RelayList()?.readRelaysNorm() ?: emptyList() relayList .map { relaySetupInfoBuilder(it) } - .distinctBy { it.url } + .distinctBy { it.relay } .sortedBy { it.relayStat.receivedBytes } .reversed() } } fun addHomeRelay(relay: BasicRelaySetupInfo) { - if (_homeRelays.value.any { it.url == relay.url }) return + if (_homeRelays.value.any { it.relay == relay.relay }) return _homeRelays.update { it.plus(relay) } hasModified = true @@ -154,7 +156,7 @@ class Nip65RelayListViewModel : ViewModel() { } fun addNotifRelay(relay: BasicRelaySetupInfo) { - if (_notificationRelays.value.any { it.url == relay.url }) return + if (_notificationRelays.value.any { it.relay == relay.relay }) return _notificationRelays.update { it.plus(relay) } hasModified = true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelaySetupInfoProposalDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelaySetupInfoProposalDialog.kt deleted file mode 100644 index 5e720bd5dc..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelaySetupInfoProposalDialog.kt +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.service.Nip11Retriever -import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache - -@Composable -fun Kind3RelaySetupInfoProposalDialog( - item: Kind3RelayProposalSetupInfo, - onAdd: (Kind3RelayProposalSetupInfo) -> Unit, - accountViewModel: AccountViewModel, - nav: INav, -) { - var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) } - val context = LocalContext.current - - relayInfo?.let { - RelayInformationDialog( - onClose = { relayInfo = null }, - relayInfo = it.relayInfo, - relayBriefInfo = it.relayBriefInfo, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - Kind3RelaySetupInfoProposalRow( - item = item, - loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, - onAdd = { - onAdd(item) - }, - accountViewModel = accountViewModel, - onClick = { - accountViewModel.retrieveRelayDocument( - item.url, - onInfo = { - relayInfo = RelayInfoDialog(RelayBriefInfoCache.RelayBriefInfo(item.url), it) - }, - onError = { url, errorCode, exceptionMessage -> - val msg = - when (errorCode) { - Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) - - Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) - - Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) - - Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - url, - exceptionMessage, - ) - } - - accountViewModel.toastManager.toast( - stringRes(context, R.string.unable_to_download_relay_document), - msg, - ) - }, - ) - }, - nav = nav, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelaySetupInfoProposalRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelaySetupInfoProposalRow.kt deleted file mode 100644 index f04bdbeef7..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/recommendations/Kind3RelaySetupInfoProposalRow.kt +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Paid -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.AddRelayButton -import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon -import com.vitorpamplona.amethyst.ui.note.UserPicture -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding -import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier -import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChatMaxWidth -import com.vitorpamplona.amethyst.ui.theme.Size25dp -import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.amethyst.ui.theme.allGoodColor -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.quartz.nip01Core.relay.RelayStat - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun Kind3RelaySetupInfoProposalRow( - item: Kind3RelayProposalSetupInfo, - loadProfilePicture: Boolean, - loadRobohash: Boolean, - onAdd: () -> Unit, - onClick: () -> Unit, - accountViewModel: AccountViewModel, - nav: INav, -) { - Column( - Modifier - .fillMaxWidth() - .clickable(onClick = onClick), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(vertical = 5.dp), - ) { - val relayInfo by loadRelayInfo(item.url, accountViewModel) - - RenderRelayIcon( - item.briefInfo.displayUrl, - relayInfo?.icon ?: item.briefInfo.favIcon, - loadProfilePicture, - loadRobohash, - item.relayStat.pingInMs, - LargeRelayIconModifier, - ) - - Spacer(modifier = HalfHorzPadding) - - Column(Modifier.weight(1f)) { - Row(ReactionRowHeightChatMaxWidth, verticalAlignment = Alignment.CenterVertically) { - Text( - text = item.briefInfo.displayUrl, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - if (item.paidRelay) { - Icon( - imageVector = Icons.Default.Paid, - null, - modifier = - Modifier - .padding(start = 5.dp, top = 1.dp) - .size(14.dp), - tint = MaterialTheme.colorScheme.allGoodColor, - ) - } - } - } - - UsedBy(item, accountViewModel, nav) - - Column( - Modifier - .padding(start = 10.dp), - ) { - AddRelayButton(onAdd) - } - } - - HorizontalDivider(thickness = DividerThickness) - } -} - -@Preview -@Composable -fun UsedByPreview() { - ThemeComparisonColumn { - UsedBy( - item = - Kind3RelayProposalSetupInfo( - "wss://nos.lol", - true, - true, - COMMON_FEED_TYPES, - relayStat = RelayStat(), - paidRelay = false, - users = listOf("User1", "User2", "User3", "User4"), - ), - accountViewModel = mockAccountViewModel(), - nav = EmptyNav, - ) - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun UsedBy( - item: Kind3RelayProposalSetupInfo, - accountViewModel: AccountViewModel, - nav: INav, -) { - FlowRow(verticalArrangement = Arrangement.Center) { - item.users.getOrNull(0)?.let { - UserPicture( - userHex = it, - size = Size25dp, - accountViewModel = accountViewModel, - nav = nav, - ) - } - item.users.getOrNull(1)?.let { - UserPicture( - userHex = it, - size = Size25dp, - accountViewModel = accountViewModel, - nav = nav, - ) - } - item.users.getOrNull(2)?.let { - UserPicture( - userHex = it, - size = Size25dp, - accountViewModel = accountViewModel, - nav = nav, - ) - } - if (item.users.size > 3) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.height(Size25dp)) { - Text( - text = stringRes(R.string.and_more, item.users.size - 3), - maxLines = 1, - ) - } - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt index bc2cc0c731..495daa68d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt @@ -65,7 +65,7 @@ fun LazyListScope.renderSearchItems( accountViewModel: AccountViewModel, nav: INav, ) { - itemsIndexed(feedState, key = { _, item -> "Search" + item.url }) { index, item -> + itemsIndexed(feedState, key = { _, item -> "Search" + item.relay }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteRelay(item) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt index d489072bbb..06edc49bc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt @@ -21,11 +21,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class SearchRelayListViewModel : BasicRelaySetupInfoModel() { - override fun getRelayList(): List? = account.getSearchRelayList()?.relays() + override fun getRelayList(): List? = account.searchRelayList.getSearchRelayList()?.relays() - override fun saveRelayList(urlList: List) { + override fun saveRelayList(urlList: List) { account.saveSearchRelayList(urlList) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt index 6ca95b192f..045d583c46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt @@ -68,7 +68,7 @@ class SearchBarViewModel( .onEach(::updateDataSource) .stateIn(viewModelScope, SharingStarted.Eagerly, searchValue) - val searchDataSourceState = SearchQueryState(MutableStateFlow(searchValue)) + val searchDataSourceState = SearchQueryState(MutableStateFlow(searchValue), account) val searchResultsUsers = combine(searchValueFlow.debounce(100), invalidations.debounce(100)) { term, version -> @@ -82,7 +82,7 @@ class SearchBarViewModel( combine(searchValueFlow.debounce(100), invalidations) { term, version -> logTime("SearchBarViewModel findNotesStartingWith") { LocalCache - .findNotesStartingWith(term, account) + .findNotesStartingWith(term, account.hiddenUsers) .sortedWith(DefaultFeedOrder) } }.flowOn(Dispatchers.Default) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index eb0fdfa253..2981f85d9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -335,8 +335,8 @@ fun WatchAccountAndBlockList( accountViewModel: AccountViewModel, invalidate: () -> Unit, ) { - val transientSpammers by accountViewModel.account.transientHiddenUsers.collectAsStateWithLifecycle() - val blockListState by accountViewModel.account.flowHiddenUsers.collectAsStateWithLifecycle() + val transientSpammers by accountViewModel.account.hiddenUsers.transientHiddenUsers.collectAsStateWithLifecycle() + val blockListState by accountViewModel.account.hiddenUsers.flow.collectAsStateWithLifecycle() LaunchedEffect(accountViewModel, transientSpammers, blockListState) { invalidate() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt index 76fb9f019d..b1c001bb8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenAccountsFeedFilter.kt @@ -35,7 +35,7 @@ class HiddenAccountsFeedFilter( override fun showHiddenKey(): Boolean = true override fun feed(): List = - account.flowHiddenUsers.value.hiddenUsers.reversed().mapNotNull { + account.hiddenUsers.flow.value.hiddenUsers.reversed().mapNotNull { try { LocalCache.getOrCreateUser(it) } catch (e: Exception) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenWordsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenWordsFeedFilter.kt index 59ffe6cb8d..ec9b509767 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenWordsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/HiddenWordsFeedFilter.kt @@ -31,6 +31,6 @@ class HiddenWordsFeedFilter( override fun showHiddenKey(): Boolean = true override fun feed(): List = - account.flowHiddenUsers.value.hiddenWords + account.hiddenUsers.flow.value.hiddenWords .toList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt index 8cf06a1d4a..69a19a8bdd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt @@ -32,5 +32,5 @@ class SpammerAccountsFeedFilter( override fun showHiddenKey(): Boolean = true - override fun feed(): List = account.transientHiddenUsers.value.map { LocalCache.getOrCreateUser(it) } + override fun feed(): List = account.hiddenUsers.transientHiddenUsers.value.map { LocalCache.getOrCreateUser(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 8e9c54cb39..3f721fbc79 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -479,7 +479,7 @@ private fun FullBleedNoteCompose( val geo = remember { noteEvent.geoHashOrScope() } if (geo != null) { - DisplayLocation(geo, nav) + DisplayLocation(geo, accountViewModel, nav) } val baseReward = remember { noteEvent.bountyBaseReward()?.let { Reward(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt index 5f94ccae19..f1ae4c13a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/dal/ThreadFeedFilter.kt @@ -39,7 +39,7 @@ class ThreadFeedFilter( override fun feed(): List { val cachedSignatures: MutableMap = mutableMapOf() - val followingKeySet = account.liveKind3Follows.value.authors + val followingKeySet = account.kind3FollowList.flow.value.authors val eventsToWatch = ThreadAssembler().findThreadFor(noteId) ?: return emptyList() // Filter out drafts made by other accounts on device diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt index 11c3948c87..3e9152ff88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadEventLoaderSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadFilterSubAssembler -import com.vitorpamplona.ammolite.relays.NostrClient import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to tags, even the same tag class ThreadQueryState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt index 0e896d20cb..c0cabe9fff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterEventsInThreadForRoot.kt @@ -22,65 +22,68 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.sub import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter fun filterEventsInThreadForRoot( root: Note, - since: Map?, -): List { + since: SincePerRelayMap?, +): List { val addressRoot = if (root is AddressableNote) root.idHex else null val eventRoot = if (root !is AddressableNote) root.idHex else root.event?.id - val address = - if (addressRoot != null) { - listOf( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("a" to listOf(addressRoot)), - since = since, - ), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("A" to listOf(addressRoot)), - since = since, - ), - ), - ) - } else { - emptyList() - } + return root.relayUrlsForReactions().toSet().flatMap { + val since = since?.get(it)?.time - val event = - if (eventRoot != null) { - listOf( - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("e" to listOf(eventRoot)), - since = since, - ), - ), - TypedFilter( - types = COMMON_FEED_TYPES, - filter = - SincePerRelayFilter( - tags = mapOf("E" to listOf(eventRoot)), - since = since, - ), - ), - ) - } else { - emptyList() - } + val addressList = + if (addressRoot != null) { + listOf( + RelayBasedFilter( + relay = it, + filter = + Filter( + tags = mapOf("a" to listOf(addressRoot)), + since = since, + ), + ), + RelayBasedFilter( + relay = it, + filter = + Filter( + tags = mapOf("A" to listOf(addressRoot)), + since = since, + ), + ), + ) + } else { + emptyList() + } - return address + event + val eventList = + if (eventRoot != null) { + listOf( + RelayBasedFilter( + relay = it, + filter = + Filter( + tags = mapOf("e" to listOf(eventRoot)), + since = since, + ), + ), + RelayBasedFilter( + relay = it, + filter = + Filter( + tags = mapOf("E" to listOf(eventRoot)), + since = since, + ), + ), + ) + } else { + emptyList() + } + + addressList + eventList + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt index 975844cf6a..2a77451434 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt @@ -24,11 +24,12 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -fun filterMissingEventsForThread(threadInfo: ThreadAssembler.ThreadInfo): List { - val missingEvents = mutableSetOf() +fun filterMissingEventsForThread(threadInfo: ThreadAssembler.ThreadInfo): List { + val missingEvents = mutableSetOf() val missingAddresses = mutableSetOf
() if (threadInfo.root.event == null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt index 0426f813c3..c6703e69de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.sub import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter /** * Loads all missing events in each thread. @@ -42,8 +42,8 @@ class ThreadEventLoaderSubAssembler( ) : PerUniqueIdEoseManager(client, allKeys, invalidateAfterEose = true) { override fun updateFilter( key: ThreadQueryState, - since: Map?, - ): List? { + since: SincePerRelayMap?, + ): List? { val branches = ThreadAssembler().findThreadFor(key.eventId) ?: return null return filterMissingEventsForThread(branches) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt index be35518598..fa3efb51bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.sub import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter /** * Loads all events in the Thread that cites the root post. @@ -39,8 +39,8 @@ class ThreadFilterSubAssembler( ) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: ThreadQueryState, - since: Map?, - ): List? { + since: SincePerRelayMap?, + ): List? { val root = ThreadAssembler().findRoot(key.eventId) ?: return null return filterEventsInThreadForRoot(root, since) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index 1245283f1c..e8f5f14c05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -164,7 +164,7 @@ fun WatchAccountForVideoScreen( accountViewModel: AccountViewModel, ) { val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle() - val hiddenUsers = accountViewModel.account.flowHiddenUsers.collectAsStateWithLifecycle() + val hiddenUsers = accountViewModel.account.hiddenUsers.flow.collectAsStateWithLifecycle() LaunchedEffect(accountViewModel, listState, hiddenUsers) { videoFeedContentState.checkKeysInvalidateDataAndSendToTop() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt index ec72c56339..3e669f0dcc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt @@ -121,10 +121,8 @@ class VideoFeedFilter( fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( - userHex = account.userProfile().pubkeyHex, - selectedListName = account.settings.defaultStoriesFollowList.value, followLists = account.liveStoriesFollowLists.value, - hiddenUsers = account.flowHiddenUsers.value, + hiddenUsers = account.hiddenUsers.flow.value, ) override fun sort(collection: Set): List = collection.sortedWith(DefaultFeedOrder) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/FeedBasis.kt similarity index 50% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByGeohash.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/FeedBasis.kt index 04d88894d4..6915b01989 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/FeedBasis.kt @@ -18,51 +18,21 @@ * 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.video.datasource.subassemblies +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.SUPPORTED_VIDEO_FEED_MIME_TYPES -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent -fun filterPictureAndVideoByGeohash( - hashToLoad: Set?, - since: Map?, -): List { - if (hashToLoad == null || hashToLoad.isEmpty()) return emptyList() +val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif") +val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet() - val geoHashes = hashToLoad.toList() +val PictureAndVideoKinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND) +val PictureAndVideoKTags = listOf(PictureEvent.KIND.toString(), VideoHorizontalEvent.KIND.toString(), VideoVerticalEvent.KIND.toString()) - return listOf( - TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), - tags = mapOf("g" to geoHashes), - limit = 100, - since = since, - ), - ), - TypedFilter( - types = setOf(FeedType.GLOBAL), - filter = - SincePerRelayFilter( - kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), - tags = - mapOf( - "g" to geoHashes, - "m" to SUPPORTED_VIDEO_FEED_MIME_TYPES, - ), - limit = 100, - since = since, - ), - ), - ) -} +val PictureAndVideoLegacyKinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND) +val PictureAndVideoLegacyKTags = listOf(FileHeaderEvent.KIND.toString(), FileStorageHeaderEvent.KIND.toString()) +val LegacyMimeTypes = SUPPORTED_VIDEO_FEED_MIME_TYPES +val LegacyMimeTypeMap = mapOf("m" to LegacyMimeTypes) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt index f1dbb2a70d..d1f95d119e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt @@ -22,14 +22,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.VideoMixGeohashHashtagsFilterSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.VideoOutboxEventsFilterSubAssembler -import com.vitorpamplona.ammolite.relays.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import kotlinx.coroutines.CoroutineScope -val SUPPORTED_VIDEO_FEED_MIME_TYPES = listOf("image/jpeg", "image/gif", "image/png", "image/webp", "video/mp4", "video/mpeg", "video/webm", "audio/aac", "audio/mpeg", "audio/webm", "audio/wav", "image/avif") -val SUPPORTED_VIDEO_FEED_MIME_TYPES_SET = SUPPORTED_VIDEO_FEED_MIME_TYPES.toSet() - // This allows multiple screen to be listening to tags, even the same tag class VideoQueryState( val account: Account, @@ -42,7 +38,6 @@ class VideoFilterAssembler( val group = listOf( VideoOutboxEventsFilterSubAssembler(client, ::allKeys), - VideoMixGeohashHashtagsFilterSubAssembler(client, ::allKeys), ) override fun start() = group.forEach { it.start() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByFollows.kt deleted file mode 100644 index 392774e8b5..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByFollows.kt +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies - -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.SUPPORTED_VIDEO_FEED_MIME_TYPES -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent - -fun filterPictureAndVideoByFollows( - follows: Map>?, - since: Map?, -): List { - if (follows != null && follows.isEmpty()) return emptyList() - - return listOf( - TypedFilter( - types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), - limit = 200, - since = since, - ), - ), - TypedFilter( - types = setOf(if (follows == null) FeedType.GLOBAL else FeedType.FOLLOWS), - filter = - SinceAuthorPerRelayFilter( - authors = follows, - kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), - limit = 200, - tags = mapOf("m" to SUPPORTED_VIDEO_FEED_MIME_TYPES), - since = since, - ), - ), - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByHashtag.kt deleted file mode 100644 index a3be33c21c..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/FilterPictureAndVideoByHashtag.kt +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies - -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.SUPPORTED_VIDEO_FEED_MIME_TYPES -import com.vitorpamplona.ammolite.relays.FeedType -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts -import com.vitorpamplona.quartz.nip68Picture.PictureEvent -import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent -import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent -import kotlin.collections.flatten - -fun filterPictureAndVideoByHashtag( - hashToLoad: Set?, - since: Map?, -): List { - if (hashToLoad == null || hashToLoad.isEmpty()) return emptyList() - - val hashtags = hashToLoad.map { hashtagAlts(it) }.flatten().distinct() - - if (hashtags.isEmpty()) return emptyList() - - return listOf( - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SincePerRelayFilter( - kinds = listOf(PictureEvent.KIND, VideoHorizontalEvent.KIND, VideoVerticalEvent.KIND), - tags = mapOf("t" to hashtags), - limit = 100, - since = since, - ), - ), - TypedFilter( - types = setOf(FeedType.FOLLOWS), - filter = - SincePerRelayFilter( - kinds = listOf(FileHeaderEvent.KIND, FileStorageHeaderEvent.KIND), - tags = - mapOf( - "t" to hashtags, - "m" to SUPPORTED_VIDEO_FEED_MIME_TYPES, - ), - limit = 100, - since = since, - ), - ), - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoMixGeohashHashtagsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoMixGeohashHashtagsFilterSubAssembler.kt deleted file mode 100644 index bcabeff4b6..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoMixGeohashHashtagsFilterSubAssembler.kt +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies - -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager -import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -class VideoMixGeohashHashtagsFilterSubAssembler( - client: NostrClient, - allKeys: () -> Set, -) : PerUserEoseManager(client, allKeys) { - override fun updateFilter( - key: VideoQueryState, - since: Map?, - ): List? = - filterPictureAndVideoByHashtag( - key.followLists()?.hashtags, - since, - ) + - filterPictureAndVideoByGeohash( - key.followLists()?.geotags, - since, - ) - - override fun user(key: VideoQueryState) = key.account.userProfile() - - fun VideoQueryState.followListsFlow() = account.liveStoriesFollowLists - - fun VideoQueryState.followLists() = followListsFlow().value - - val userJobMap = mutableMapOf() - - override fun newSub(key: VideoQueryState): Subscription { - userJobMap[key.account.userProfile()]?.cancel() - userJobMap[key.account.userProfile()] = - key.scope.launch(Dispatchers.Default) { - key.followListsFlow().collectLatest { - invalidateFilters() - } - } - - return super.newSub(key) - } - - override fun endSub( - key: User, - subId: String, - ) { - return super.endSub(key, subId) - userJobMap[key]?.cancel() - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt index 0cc3cbd003..cc5787e9b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt @@ -21,12 +21,27 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoQueryState -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.TypedFilter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoByGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoByHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoGlobal +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip65Follows.filterPictureAndVideoByAuthors +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip65Follows.filterPictureAndVideoByFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities.filterPictureAndVideoByAllCommunities +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities.filterPictureAndVideoByCommunity import com.vitorpamplona.ammolite.relays.datasources.Subscription -import com.vitorpamplona.ammolite.relays.filters.EOSETime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job @@ -40,8 +55,21 @@ class VideoOutboxEventsFilterSubAssembler( ) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: VideoQueryState, - since: Map?, - ): List? = filterPictureAndVideoByFollows(key.followsPerRelay(), since) + since: SincePerRelayMap?, + ): List? { + val feedSettings = key.followsPerRelay() + return when (feedSettings) { + is AllCommunitiesTopNavPerRelayFilterSet -> filterPictureAndVideoByAllCommunities(feedSettings, since) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByFollows(feedSettings, since) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since) + is GlobalTopNavPerRelayFilterSet -> filterPictureAndVideoGlobal(feedSettings, since) + is HashtagTopNavPerRelayFilterSet -> filterPictureAndVideoByHashtag(feedSettings, since) + is LocationTopNavPerRelayFilterSet -> filterPictureAndVideoByGeohash(feedSettings, since) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since) + is SingleCommunityTopNavPerRelayFilterSet -> filterPictureAndVideoByCommunity(feedSettings, since) + else -> emptyList() + } + } override fun user(key: VideoQueryState) = key.account.userProfile() @@ -51,11 +79,7 @@ class VideoOutboxEventsFilterSubAssembler( fun VideoQueryState.listName() = listNameFlow().value - fun VideoQueryState.followListsFlow() = account.liveStoriesFollowLists - - fun VideoQueryState.followLists() = followListsFlow().value - - fun VideoQueryState.followsPerRelayFlow() = account.liveStoriesListAuthorsPerRelay + fun VideoQueryState.followsPerRelayFlow() = account.liveStoriesFollowListsPerRelay fun VideoQueryState.followsPerRelay() = followsPerRelayFlow().value @@ -67,11 +91,6 @@ class VideoOutboxEventsFilterSubAssembler( userJobMap[user]?.forEach { it.cancel() } userJobMap[user] = listOf( - key.scope.launch(Dispatchers.Default) { - key.listNameFlow().collectLatest { - invalidateFilters() - } - }, key.scope.launch(Dispatchers.Default) { key.followsPerRelayFlow().sample(5000).collectLatest { invalidateFilters() @@ -86,7 +105,7 @@ class VideoOutboxEventsFilterSubAssembler( key: User, subId: String, ) { - return super.endSub(key, subId) + super.endSub(key, subId) userJobMap[key]?.forEach { it.cancel() } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt new file mode 100644 index 0000000000..277c21e6bc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core + +import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.LegacyMimeTypes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoKinds +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoLegacyKinds +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlin.collections.flatten +import kotlin.collections.mapNotNull + +fun filterPictureAndVideoGeohash( + relay: NormalizedRelayUrl, + geotags: Set, + since: Long? = null, +): List { + val geoHashes = geotags.sorted() + + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = PictureAndVideoKinds, + tags = mapOf("g" to geoHashes), + limit = 400, + since = since, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = PictureAndVideoLegacyKinds, + tags = + mapOf( + "g" to geoHashes, + "m" to LegacyMimeTypes, + ), + limit = 200, + since = since, + ), + ), + ) +} + +fun filterPictureAndVideoByGeohash( + geoSet: LocationTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (geoSet.set.isEmpty()) return emptyList() + + return geoSet.set.mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterPictureAndVideoGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt new file mode 100644 index 0000000000..12dc9003e1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core + +import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.LegacyMimeTypes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoKinds +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoLegacyKinds +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterPictureAndVideoHashtag( + relay: NormalizedRelayUrl, + hashtags: Set, + since: Long? = null, +): List { + val hashtags = hashtags.flatMap(::hashtagAlts).distinct().sorted() + + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = PictureAndVideoKinds, + tags = mapOf("t" to hashtags), + limit = 400, + since = since, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = PictureAndVideoLegacyKinds, + tags = + mapOf( + "t" to hashtags, + "m" to LegacyMimeTypes, + ), + limit = 200, + since = since, + ), + ), + ) +} + +fun filterPictureAndVideoByHashtag( + hashSet: HashtagTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (hashSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return hashSet.set.mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterPictureAndVideoHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt new file mode 100644 index 0000000000..674dd199d6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core + +import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.LegacyMimeTypeMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoKinds +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoLegacyKinds +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter + +fun filterPictureAndVideoGlobal( + relays: GlobalTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (relays.set.isEmpty()) return emptyList() + + return relays.set.flatMap { + val since = since?.get(it.key)?.time + listOf( + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = PictureAndVideoKinds, + limit = 200, + since = since, + ), + ), + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = PictureAndVideoLegacyKinds, + tags = LegacyMimeTypeMap, + limit = 200, + since = since, + ), + ), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt new file mode 100644 index 0000000000..7b490dcad9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip65Follows + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.LegacyMimeTypeMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoKinds +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoLegacyKinds +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten +import kotlin.collections.mapNotNull + +fun filterPictureAndVideoAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + val authorList = authors.sorted() + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authorList, + kinds = PictureAndVideoKinds, + limit = 400, + since = since, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authorList, + kinds = PictureAndVideoLegacyKinds, + tags = LegacyMimeTypeMap, + limit = 200, + since = since, + ), + ), + ) +} + +fun filterPictureAndVideoByAuthors( + authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterPictureAndVideoAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} + +fun filterPictureAndVideoByAuthors( + authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (authorSet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return authorSet.set.mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterPictureAndVideoAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt new file mode 100644 index 0000000000..55bb3deae6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip65Follows + +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoGeohash +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoHashtag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities.filterPictureAndVideoAllCommunities +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import kotlin.collections.flatten + +fun filterPictureAndVideoByFollows( + followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (followsSet.set.isEmpty()) return emptyList() + + return followsSet.set.flatMap { + val since = since?.get(it.key)?.time + val relay = it.key + + listOfNotNull( + it.value.authors?.let { + filterPictureAndVideoAuthors(relay, it, since) + }, + it.value.geotags?.let { + filterPictureAndVideoGeohash(relay, it, since) + }, + it.value.hashtags?.let { + filterPictureAndVideoHashtag(relay, it, since) + }, + it.value.communities?.let { + filterPictureAndVideoAllCommunities(relay, it, since) + }, + ).flatten() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt new file mode 100644 index 0000000000..08071cb909 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.LegacyMimeTypes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoKTags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoKinds +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoLegacyKinds +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent +import kotlin.collections.flatten + +fun filterPictureAndVideoAllCommunities( + relay: NormalizedRelayUrl, + communities: Set, + since: Long? = null, +): List { + val communityList = communities.sorted() + + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to communityList, + "k" to PictureAndVideoKTags, + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = mapOf("a" to communityList), + kinds = PictureAndVideoKinds, + limit = 300, + since = since, + ), + ), + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to communityList, + "k" to listOf(FileHeaderEvent.KIND.toString(), FileStorageHeaderEvent.KIND.toString()), + ), + limit = 300, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + tags = + mapOf( + "a" to communityList, + "m" to LegacyMimeTypes, + ), + kinds = PictureAndVideoLegacyKinds, + limit = 300, + since = since, + ), + ), + ) +} + +fun filterPictureAndVideoByAllCommunities( + communitySet: AllCommunitiesTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + return communitySet.set.mapNotNull { + filterPictureAndVideoAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time, + ) + }.flatten() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt new file mode 100644 index 0000000000..61aee43e22 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities + +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.LegacyMimeTypes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoKTags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoKinds +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoLegacyKTags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoLegacyKinds +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten + +fun filterPictureAndVideoCommunity( + relay: NormalizedRelayUrl, + community: String, + authors: Set?, + since: Long? = null, +): List { + val authors = authors?.sorted() + return listOf( + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to listOf(community), + "k" to PictureAndVideoKTags, + ), + limit = 500, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + tags = mapOf("a" to listOf(community)), + kinds = PictureAndVideoKinds, + limit = 500, + since = since, + ), + ), + // approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + kinds = CommunityPostApprovalEvent.KIND_LIST, + tags = + mapOf( + "a" to listOf(community), + "k" to PictureAndVideoLegacyKTags, + ), + limit = 500, + since = since, + ), + ), + // not approved + RelayBasedFilter( + relay = relay, + filter = + Filter( + authors = authors, + tags = + mapOf( + "a" to listOf(community), + "m" to LegacyMimeTypes, + ), + kinds = PictureAndVideoLegacyKinds, + limit = 500, + since = since, + ), + ), + ) +} + +fun filterPictureAndVideoByCommunity( + communitySet: SingleCommunityTopNavPerRelayFilterSet, + since: SincePerRelayMap?, +): List { + if (communitySet.set.isEmpty()) return emptyList() + + val defaultSince = TimeUtils.oneWeekAgo() + + return communitySet.set.mapNotNull { + filterPictureAndVideoCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() +} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Constants.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Constants.kt deleted file mode 100644 index 720a302c7b..0000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Constants.kt +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.relays - -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter - -object Constants { - val activeTypesFollows = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS) - val activeTypesChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS) - val activeTypesGlobalChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL) - val activeTypesSearch = setOf(FeedType.SEARCH) - - val defaultRelays = - arrayOf( - // Free relays for only DMs, Chats and Follows due to the amount of spam - RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.bitcoiner.social"), read = true, write = true, feedTypes = activeTypesChats), - RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.nostr.bg"), read = true, write = true, feedTypes = activeTypesChats), - RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.oxtr.dev"), read = true, write = true, feedTypes = activeTypesChats), - RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.fmt.wiz.biz"), read = true, write = false, feedTypes = activeTypesChats), - RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.damus.io"), read = true, write = true, feedTypes = activeTypesFollows), - // Global - RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.mom"), read = true, write = true, feedTypes = activeTypesGlobalChats), - RelaySetupInfo(RelayUrlFormatter.normalize("wss://nos.lol"), read = true, write = true, feedTypes = activeTypesGlobalChats), - // Paid relays - RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostrelites.org"), read = true, write = false, feedTypes = activeTypesGlobalChats), - RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.wine"), read = true, write = false, feedTypes = activeTypesGlobalChats), - // Supporting NIP-50 - RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.nostr.band"), read = true, write = false, feedTypes = activeTypesSearch), - RelaySetupInfo(RelayUrlFormatter.normalize("wss://nostr.wine"), read = true, write = false, feedTypes = activeTypesSearch), - RelaySetupInfo(RelayUrlFormatter.normalize("wss://relay.noswhere.com"), read = true, write = false, feedTypes = activeTypesSearch), - ) - - val defaultSearchRelaySet = - setOf( - RelayUrlFormatter.normalize("wss://relay.nostr.band"), - RelayUrlFormatter.normalize("wss://nostr.wine"), - RelayUrlFormatter.normalize("wss://relay.noswhere.com"), - ) -} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt deleted file mode 100644 index 9fb98f5569..0000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/NostrClient.kt +++ /dev/null @@ -1,475 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.relays - -import android.util.Log -import com.vitorpamplona.ammolite.service.checkNotInMainThread -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.RelayState -import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import java.util.UUID -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit - -/** - * The Nostr Client manages a relay pool - */ -class NostrClient( - private val websocketBuilder: WebsocketBuilderFactory, -) : RelayPool.Listener { - private val relayPool: RelayPool = RelayPool() - private val activeSubscriptions: MutableSubscriptionCache = MutableSubscriptionCache() - private var listeners = setOf() - - fun buildRelay(it: RelaySetupInfoToConnect): Relay = Relay(it.url, it.read, it.write, it.forceProxy, it.feedTypes, websocketBuilder, activeSubscriptions) - - fun getRelay(url: String): Relay? = relayPool.getRelay(url) - - // Reconnects all relays that may have disconnected - fun reconnect() = relayPool.requestAndWatch() - - @Synchronized - fun reconnect( - relays: Array?, - onlyIfChanged: Boolean = false, - ) { - Log.d("Relay", "Relay Pool Reconnecting to ${relays?.size} relays: \n${relays?.joinToString("\n") { it.url + " " + it.forceProxy + " " + it.read + " " + it.write + " " + it.feedTypes.joinToString(",") { it.name } }}") - checkNotInMainThread() - - if (onlyIfChanged) { - if (!isSameRelaySetConfig(relays)) { - if (this.relayPool.availableRelays() > 0) { - relayPool.disconnect() - relayPool.unregister(this) - relayPool.unloadRelays() - } - - if (relays != null) { - relayPool.register(this) - relayPool.loadRelays(relays.map(::buildRelay)) - relayPool.requestAndWatch() - } - } else { - // Reconnects all relays that may have disconnected - relayPool.requestAndWatch() - } - } else { - if (this.relayPool.availableRelays() > 0) { - relayPool.disconnect() - relayPool.unregister(this) - relayPool.unloadRelays() - } - - if (relays != null) { - relayPool.register(this) - relayPool.loadRelays(relays.map(::buildRelay)) - relayPool.requestAndWatch() - } - } - } - - fun isSameRelaySetConfig(newRelayConfig: Array?): Boolean { - if (relayPool.availableRelays() != newRelayConfig?.size) return false - - newRelayConfig.forEach { - val relay = relayPool.getRelay(it.url) ?: return false - if (!relay.isSameRelayConfig(it)) return false - } - - return true - } - - fun sendFilter( - subscriptionId: String = UUID.randomUUID().toString().substring(0..10), - filters: List = listOf(), - ) { - checkNotInMainThread() - - activeSubscriptions.add(subscriptionId, filters) - relayPool.sendFilter(subscriptionId, filters) - } - - fun sendFilterAndStopOnFirstResponse( - subscriptionId: String = UUID.randomUUID().toString().substring(0..10), - filters: List = listOf(), - onResponse: (Event) -> Unit, - ) { - checkNotInMainThread() - - subscribe( - object : Listener { - override fun onEvent( - event: Event, - subId: String, - relay: Relay, - arrivalTime: Long, - afterEOSE: Boolean, - ) { - if (subId == subscriptionId) { - unsubscribe(this) - close(subscriptionId) - - onResponse(event) - } - } - }, - ) - - activeSubscriptions.add(subscriptionId, filters) - relayPool.sendFilter(subscriptionId, filters) - } - - @OptIn(DelicateCoroutinesApi::class) - suspend fun sendAndWaitForResponse( - signedEvent: Event, - relay: String? = null, - forceProxy: Boolean = false, - feedTypes: Set? = null, - relayList: List? = null, - onDone: (() -> Unit)? = null, - additionalListener: Listener? = null, - timeoutInSeconds: Long = 15, - ): Boolean { - checkNotInMainThread() - - val size = if (relay != null) 1 else relayList?.size ?: relayPool.availableRelays() - val latch = CountDownLatch(size) - val relayErrors = mutableMapOf() - var result = false - - Log.d("sendAndWaitForResponse", "Waiting for $size responses") - - val subscription = - object : Listener { - override fun onError( - error: Error, - subscriptionId: String, - relay: Relay, - ) { - relayErrors[relay.url]?.let { - latch.countDown() - } - Log.d("sendAndWaitForResponse", "onError Error from relay ${relay.url} count: ${latch.count} error: $error") - } - - override fun onEOSE( - relay: Relay, - subscriptionId: String, - arrivalTime: Long, - ) { - latch.countDown() - Log.d("sendAndWaitForResponse", "onEOSE relay ${relay.url} count: ${latch.count}") - } - - override fun onRelayStateChange( - type: RelayState, - relay: Relay, - ) { - if (type == RelayState.DISCONNECTED) { - latch.countDown() - } - if (type == RelayState.CONNECTED) { - Log.d("sendAndWaitForResponse", "${type.name} Sending event to relay ${relay.url} count: ${latch.count}") - relay.send(signedEvent) - } - Log.d("sendAndWaitForResponse", "onRelayStateChange ${type.name} from relay ${relay.url} count: ${latch.count}") - } - - override fun onSendResponse( - eventId: String, - success: Boolean, - message: String, - relay: Relay, - ) { - if (eventId == signedEvent.id) { - if (success) { - result = true - } - latch.countDown() - Log.d("sendAndWaitForResponse", "onSendResponse Received response for $eventId from relay ${relay.url} count: ${latch.count} message $message success $success") - } - } - } - - subscribe(subscription) - additionalListener?.let { subscribe(it) } - - val job = - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { - if (relayList != null) { - send(signedEvent, relayList) - } else if (relay == null) { - send(signedEvent) - } else { - sendSingle(signedEvent, RelaySetupInfoToConnect(relay, forceProxy, true, true, emptySet()), onDone ?: {}) - } - } - job.join() - - runBlocking { - latch.await(timeoutInSeconds, TimeUnit.SECONDS) - } - Log.d("sendAndWaitForResponse", "countdown finished") - unsubscribe(subscription) - additionalListener?.let { unsubscribe(it) } - return result - } - - fun getAll(): List = relayPool.getAll() - - fun sendFilterOnlyIfDisconnected( - subscriptionId: String = UUID.randomUUID().toString().substring(0..10), - filters: List = listOf(), - ) { - checkNotInMainThread() - - activeSubscriptions.add(subscriptionId, filters) - relayPool.connectAndSendFiltersIfDisconnected() - } - - fun sendIfExists( - signedEvent: Event, - connectedRelay: Relay, - ) { - checkNotInMainThread() - - relayPool.getRelays(connectedRelay.url).forEach { - it.send(signedEvent) - } - } - - fun sendSingle( - signedEvent: Event, - relayTemplate: RelaySetupInfoToConnect, - onDone: (() -> Unit), - ) { - checkNotInMainThread() - - relayPool.runCreatingIfNeeded(buildRelay(relayTemplate), onDone = onDone) { - it.send(signedEvent) - } - } - - fun send(signedEvent: Event) { - checkNotInMainThread() - relayPool.send(signedEvent) - } - - fun send( - signedEvent: Event, - relayList: List, - ) { - checkNotInMainThread() - - relayPool.sendToSelectedRelays(relayList, signedEvent) - } - - fun sendPrivately( - signedEvent: Event, - relayList: List, - ) { - checkNotInMainThread() - - relayList.forEach { relayTemplate -> - relayPool.runCreatingIfNeeded(buildRelay(relayTemplate)) { - it.sendOverride(signedEvent) - } - } - } - - fun close(subscriptionId: String) { - relayPool.close(subscriptionId) - activeSubscriptions.remove(subscriptionId) - } - - fun isActive(subscriptionId: String): Boolean = activeSubscriptions.isActive(subscriptionId) - - @OptIn(DelicateCoroutinesApi::class) - override fun onEvent( - event: Event, - subscriptionId: String, - relay: Relay, - arrivalTime: Long, - afterEOSE: Boolean, - ) { - // Releases the Web thread for the new payload. - // May need to add a processing queue if processing new events become too costly. - listeners.forEach { it.onEvent(event, subscriptionId, relay, arrivalTime, afterEOSE) } - } - - override fun onEOSE( - relay: Relay, - subscriptionId: String, - arrivalTime: Long, - ) { - listeners.forEach { it.onEOSE(relay, subscriptionId, arrivalTime) } - } - - override fun onRelayStateChange( - type: RelayState, - relay: Relay, - ) { - listeners.forEach { it.onRelayStateChange(type, relay) } - } - - @OptIn(DelicateCoroutinesApi::class) - override fun onSendResponse( - eventId: String, - success: Boolean, - message: String, - relay: Relay, - ) { - // Releases the Web thread for the new payload. - // May need to add a processing queue if processing new events become too costly. - listeners.forEach { it.onSendResponse(eventId, success, message, relay) } - } - - @OptIn(DelicateCoroutinesApi::class) - override fun onAuth( - relay: Relay, - challenge: String, - ) { - // Releases the Web thread for the new payload. - // May need to add a processing queue if processing new events become too costly. - listeners.forEach { it.onAuth(relay, challenge) } - } - - @OptIn(DelicateCoroutinesApi::class) - override fun onNotify( - relay: Relay, - description: String, - ) { - // Releases the Web thread for the new payload. - // May need to add a processing queue if processing new events become too costly. - listeners.forEach { it.onNotify(relay, description) } - } - - @OptIn(DelicateCoroutinesApi::class) - override fun onSend( - relay: Relay, - msg: String, - success: Boolean, - ) { - listeners.forEach { it.onSend(relay, msg, success) } - } - - @OptIn(DelicateCoroutinesApi::class) - override fun onBeforeSend( - relay: Relay, - event: Event, - ) { - listeners.forEach { it.onBeforeSend(relay, event) } - } - - @OptIn(DelicateCoroutinesApi::class) - override fun onError( - error: Error, - subscriptionId: String, - relay: Relay, - ) { - listeners.forEach { it.onError(error, subscriptionId, relay) } - } - - fun subscribe(listener: Listener) { - listeners = listeners.plus(listener) - } - - fun isSubscribed(listener: Listener): Boolean = listeners.contains(listener) - - fun unsubscribe(listener: Listener) { - listeners = listeners.minus(listener) - } - - fun allSubscriptions(): Map> = activeSubscriptions.allSubscriptions() - - fun getSubscriptionFilters(subId: String): List = activeSubscriptions.getSubscriptionFilters(subId) - - fun getSubscriptionFiltersOrNull(subId: String): List? = activeSubscriptions.getSubscriptionFiltersOrNull(subId) - - fun connectedRelays() = relayPool.connectedRelays() - - fun relayStatusFlow() = relayPool.statusFlow - - interface Listener { - /** A new message was received */ - open fun onEvent( - event: Event, - subscriptionId: String, - relay: Relay, - arrivalTime: Long, - afterEOSE: Boolean, - ) = Unit - - /** Connected to or disconnected from a relay */ - open fun onEOSE( - relay: Relay, - subscriptionId: String, - arrivalTime: Long, - ) = Unit - - /** Connected to or disconnected from a relay */ - open fun onRelayStateChange( - type: RelayState, - relay: Relay, - ) = Unit - - /** When an relay saves or rejects a new event. */ - open fun onSendResponse( - eventId: String, - success: Boolean, - message: String, - relay: Relay, - ) = Unit - - open fun onAuth( - relay: Relay, - challenge: String, - ) = Unit - - open fun onNotify( - relay: Relay, - description: String, - ) = Unit - - open fun onSend( - relay: Relay, - msg: String, - success: Boolean, - ) = Unit - - open fun onBeforeSend( - relay: Relay, - event: Event, - ) = Unit - - open fun onError( - error: Error, - subscriptionId: String, - relay: Relay, - ) = Unit - } -} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt deleted file mode 100644 index aeb7cf6320..0000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/Relay.kt +++ /dev/null @@ -1,274 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.relays - -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.RelayState -import com.vitorpamplona.quartz.nip01Core.relay.SimpleClientRelay -import com.vitorpamplona.quartz.nip01Core.relay.SubscriptionCollection -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilderFactory -import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent - -enum class FeedType { - FOLLOWS, - PUBLIC_CHATS, - PRIVATE_DMS, - GLOBAL, - SEARCH, - WALLET_CONNECT, -} - -val ALL_FEED_TYPES = - setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL, FeedType.SEARCH) - -val COMMON_FEED_TYPES = - setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL) - -val EVENT_FINDER_TYPES = - setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.GLOBAL) - -class RelaySubFilter( - val url: String, - val activeTypes: Set, - val subs: SubscriptionCache, -) : SubscriptionCollection { - fun isMatch(filter: TypedFilter) = activeTypes.any { it in filter.types } && filter.isValidFor(url) - - fun match(filters: List): Boolean = - filters.any { filter -> - isMatch(filter) - } - - override fun isActive(subscriptionId: String): Boolean = subs.isActive(subscriptionId) && match(subs.getSubscriptionFilters(subscriptionId)) - - override fun getFilters(subscriptionId: String) = filter(subs.getSubscriptionFilters(subscriptionId)) - - override fun allSubscriptions(): List = - subs.allSubscriptions().mapNotNull { filter -> - val filters = filter(filter.value) - if (filters.isNotEmpty()) { - com.vitorpamplona.quartz.nip01Core.relay - .Subscription(filter.key, filters) - } else { - null - } - } - - override fun match( - subscriptionId: String, - event: Event, - ): Boolean = subs.getSubscriptionFilters(subscriptionId).any { it.filter.match(event, url) } - - fun filter(filters: List): List = - filters.mapNotNull { filter -> - if (isMatch(filter)) { - filter.filter.toRelay(url) - } else { - null - } - } -} - -class Relay( - val url: String, - val read: Boolean = true, - val write: Boolean = true, - val forceProxy: Boolean = false, - val activeTypes: Set, - socketBuilderFactory: WebsocketBuilderFactory, - subs: SubscriptionCache, -) : SimpleClientRelay.Listener { - private var listeners = setOf() - - val relaySubFilter = RelaySubFilter(url, activeTypes, subs) - val inner = SimpleClientRelay(url, socketBuilderFactory.build(url, forceProxy), relaySubFilter, this@Relay, RelayStats.get(url)) - - val brief = RelayBriefInfoCache.get(url) - - fun register(listener: Listener) { - listeners = listeners.plus(listener) - } - - fun unregister(listener: Listener) { - listeners = listeners.minus(listener) - } - - fun isConnected() = inner.isConnected() - - fun connect() = inner.connect() - - fun connectAndRunAfterSync(onConnected: () -> Unit) { - // BRB is crashing OkHttp Deflater object :( - if (url.contains("brb.io")) return - - inner.connectAndRunAfterSync(onConnected) - } - - fun sendOutbox() = inner.sendOutbox() - - fun disconnect() = inner.disconnect() - - fun sendFilter( - requestId: String, - filters: List, - ) { - if (read) { - inner.sendRequest(requestId, relaySubFilter.filter(filters)) - } - } - - fun connectAndSendFiltersIfDisconnected() = inner.connectAndSendFiltersIfDisconnected() - - fun renewFilters() = inner.renewSubscriptions() - - fun sendOverride(signedEvent: Event) = inner.send(signedEvent) - - fun send(signedEvent: Event) { - if (signedEvent is RelayAuthEvent || write) { - inner.send(signedEvent) - } - } - - fun close(subscriptionId: String) = inner.close(subscriptionId) - - fun isSameRelayConfig(other: RelaySetupInfoToConnect): Boolean = - url == other.url && - forceProxy == other.forceProxy && - write == other.write && - read == other.read && - activeTypes == other.feedTypes - - override fun onEvent( - relay: SimpleClientRelay, - subscriptionId: String, - event: Event, - time: Long, - afterEOSE: Boolean, - ) = listeners.forEach { it.onEvent(this, subscriptionId, event, time, afterEOSE) } - - override fun onError( - relay: SimpleClientRelay, - subscriptionId: String, - error: Error, - ) = listeners.forEach { it.onError(this, subscriptionId, error) } - - override fun onEOSE( - relay: SimpleClientRelay, - subscriptionId: String, - time: Long, - ) = listeners.forEach { it.onEOSE(this, subscriptionId, time) } - - override fun onRelayStateChange( - relay: SimpleClientRelay, - type: RelayState, - ) = listeners.forEach { it.onRelayStateChange(this, type) } - - override fun onSendResponse( - relay: SimpleClientRelay, - eventId: String, - success: Boolean, - message: String, - ) = listeners.forEach { it.onSendResponse(this, eventId, success, message) } - - override fun onAuth( - relay: SimpleClientRelay, - challenge: String, - ) = listeners.forEach { it.onAuth(this, challenge) } - - override fun onNotify( - relay: SimpleClientRelay, - description: String, - ) = listeners.forEach { it.onNotify(this, description) } - - override fun onClosed( - relay: SimpleClientRelay, - subscriptionId: String, - message: String, - ) { } - - override fun onSend( - relay: SimpleClientRelay, - msg: String, - success: Boolean, - ) = listeners.forEach { it.onSend(this, msg, success) } - - override fun onBeforeSend( - relay: SimpleClientRelay, - event: Event, - ) = listeners.forEach { it.onBeforeSend(this, event) } - - interface Listener { - fun onEvent( - relay: Relay, - subscriptionId: String, - event: Event, - time: Long, - afterEOSE: Boolean, - ) - - fun onEOSE( - relay: Relay, - subscriptionId: String, - time: Long, - ) - - fun onError( - relay: Relay, - subscriptionId: String, - error: Error, - ) - - fun onSendResponse( - relay: Relay, - eventId: String, - success: Boolean, - message: String, - ) - - fun onAuth( - relay: Relay, - challenge: String, - ) - - fun onRelayStateChange( - relay: Relay, - type: RelayState, - ) - - /** Relay sent a notification */ - fun onNotify( - relay: Relay, - description: String, - ) - - fun onBeforeSend( - relay: Relay, - event: Event, - ) - - fun onSend( - relay: Relay, - msg: String, - success: Boolean, - ) - } -} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayPool.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayPool.kt deleted file mode 100644 index 821c9119ef..0000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayPool.kt +++ /dev/null @@ -1,311 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.relays - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.ammolite.service.checkNotInMainThread -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.RelayState -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch - -/** - * RelayPool manages the connection to multiple Relays and lets consumers deal with simple events. - */ -class RelayPool : Relay.Listener { - private var relays = listOf() - private var listeners = setOf() - - // Backing property to avoid flow emissions from other classes - private var lastStatus = RelayPoolStatus(0, 0) - private val _statusFlow = - MutableSharedFlow(1, 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - val statusFlow: SharedFlow = _statusFlow.asSharedFlow() - - fun availableRelays(): Int = relays.size - - fun connectedRelays(): Int = relays.count { it.isConnected() } - - fun getRelay(url: String): Relay? = relays.firstOrNull { it.url == url } - - fun getRelays(url: String): List = relays.filter { it.url == url } - - fun getAll() = relays - - fun runCreatingIfNeeded( - relay: Relay, - timeout: Long = 60000, - onDone: (() -> Unit)? = null, - whenConnected: (Relay) -> Unit, - ) { - synchronized(this) { - val matching = getRelays(relay.url) - if (matching.isNotEmpty()) { - matching.forEach { whenConnected(it) } - } else { - addRelay(relay) - - relay.connectAndRunAfterSync { - whenConnected(relay) - - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { - delay(timeout) // waits for a reply - relay.disconnect() - removeRelay(relay) - - if (onDone != null) { - onDone() - } - } - } - } - } - } - - fun loadRelays(relayList: List) { - check(relayList.isNotEmpty()) { "Relay list should never be empty" } - relayList.forEach { addRelayInner(it) } - updateStatus() - } - - fun unloadRelays() { - relays.forEach { it.unregister(this) } - relays = listOf() - } - - fun requestAndWatch() { - checkNotInMainThread() - - relays.forEach { it.connect() } - } - - fun sendFilter( - subscriptionId: String, - filters: List, - ) { - relays.forEach { relay -> - relay.sendFilter(subscriptionId, filters) - } - } - - fun connectAndSendFiltersIfDisconnected() { - relays.forEach { it.connectAndSendFiltersIfDisconnected() } - } - - fun sendToSelectedRelays( - list: List, - signedEvent: Event, - ) { - list.forEach { relay -> relays.filter { it.url == relay.url }.forEach { it.sendOverride(signedEvent) } } - } - - fun send(signedEvent: Event) { - relays.forEach { it.send(signedEvent) } - } - - fun sendOverride(signedEvent: Event) { - relays.forEach { it.sendOverride(signedEvent) } - } - - fun close(subscriptionId: String) { - relays.forEach { it.close(subscriptionId) } - } - - fun disconnect() { - relays.forEach { it.disconnect() } - } - - fun addRelay(relay: Relay) { - addRelayInner(relay) - updateStatus() - } - - private fun addRelayInner(relay: Relay) { - relay.register(this) - relays += relay - } - - fun removeRelay(relay: Relay) { - relay.unregister(this) - relays = relays.minus(relay) - updateStatus() - } - - fun register(listener: Listener) { - listeners = listeners.plus(listener) - } - - fun unregister(listener: Listener) { - listeners = listeners.minus(listener) - } - - interface Listener { - fun onEvent( - event: Event, - subscriptionId: String, - relay: Relay, - arrivalTime: Long, - afterEOSE: Boolean, - ) - - fun onEOSE( - relay: Relay, - subscriptionId: String, - arrivalTime: Long, - ) - - fun onRelayStateChange( - type: RelayState, - relay: Relay, - ) - - fun onSendResponse( - eventId: String, - success: Boolean, - message: String, - relay: Relay, - ) - - fun onAuth( - relay: Relay, - challenge: String, - ) - - fun onNotify( - relay: Relay, - description: String, - ) - - fun onSend( - relay: Relay, - msg: String, - success: Boolean, - ) - - fun onBeforeSend( - relay: Relay, - event: Event, - ) - - fun onError( - error: Error, - subscriptionId: String, - relay: Relay, - ) - } - - override fun onEvent( - relay: Relay, - subscriptionId: String, - event: Event, - arrivalTime: Long, - afterEOSE: Boolean, - ) { - listeners.forEach { it.onEvent(event, subscriptionId, relay, arrivalTime, afterEOSE) } - } - - override fun onError( - relay: Relay, - subscriptionId: String, - error: Error, - ) { - listeners.forEach { it.onError(error, subscriptionId, relay) } - updateStatus() - } - - override fun onEOSE( - relay: Relay, - subscriptionId: String, - arrivalTime: Long, - ) { - listeners.forEach { it.onEOSE(relay, subscriptionId, arrivalTime) } - updateStatus() - } - - override fun onRelayStateChange( - relay: Relay, - type: RelayState, - ) { - listeners.forEach { it.onRelayStateChange(type, relay) } - } - - override fun onSendResponse( - relay: Relay, - eventId: String, - success: Boolean, - message: String, - ) { - listeners.forEach { it.onSendResponse(eventId, success, message, relay) } - } - - override fun onAuth( - relay: Relay, - challenge: String, - ) { - listeners.forEach { it.onAuth(relay, challenge) } - } - - override fun onNotify( - relay: Relay, - description: String, - ) { - listeners.forEach { it.onNotify(relay, description) } - } - - override fun onSend( - relay: Relay, - msg: String, - success: Boolean, - ) { - listeners.forEach { it.onSend(relay, msg, success) } - } - - override fun onBeforeSend( - relay: Relay, - event: Event, - ) { - listeners.forEach { it.onBeforeSend(relay, event) } - } - - private fun updateStatus() { - val connected = connectedRelays() - val available = availableRelays() - if (lastStatus.connected != connected || lastStatus.available != available) { - lastStatus = RelayPoolStatus(connected, available) - _statusFlow.tryEmit(lastStatus) - } - } -} - -@Immutable -data class RelayPoolStatus( - val connected: Int, - val available: Int, - val isConnected: Boolean = connected > 0, -) diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/NostrDataSource.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/NostrDataSource.kt deleted file mode 100644 index 8bfb03ee41..0000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/NostrDataSource.kt +++ /dev/null @@ -1,276 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.relays.datasources - -import android.util.Log -import com.vitorpamplona.ammolite.relays.BundledUpdate -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.Relay -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.RelayState -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import java.util.concurrent.atomic.AtomicBoolean - -/** - * Semantically groups Nostr filters and subscriptions in data source objects that - * maintain the desired active filter with the relay. - * - * This model also allows each datasource to observe any new events from the network, - * regardless of the subscription - */ -abstract class NostrDataSource( - val client: NostrClient, -) { - private var subscriptions = SubscriptionSet() - val stats = SubscriptionStats() - - var changingFilters = AtomicBoolean() - - private var active: Boolean = false - - private val clientListener = - object : NostrClient.Listener { - override fun onEvent( - event: Event, - subscriptionId: String, - relay: Relay, - arrivalTime: Long, - afterEOSE: Boolean, - ) { - if (subscriptions.contains(subscriptionId)) { - stats.add(subscriptionId, event.kind) - - consume(event, relay) - - if (afterEOSE) { - markAsEOSE(subscriptionId, relay, arrivalTime) - } - } - } - - override fun onEOSE( - relay: Relay, - subscriptionId: String, - arrivalTime: Long, - ) { - if (subscriptions.contains(subscriptionId)) { - markAsEOSE(subscriptionId, relay, arrivalTime) - } - } - - override fun onRelayStateChange( - type: RelayState, - relay: Relay, - ) {} - - override fun onSendResponse( - eventId: String, - success: Boolean, - message: String, - relay: Relay, - ) { - if (success) { - markAsSeenOnRelay(eventId, relay) - } - } - - override fun onAuth( - relay: Relay, - challenge: String, - ) { - auth(relay, challenge) - } - - override fun onNotify( - relay: Relay, - description: String, - ) { - notify(relay, description) - } - } - - init { - Log.d("${this.javaClass.simpleName}", "Init, Subscribe") - client.subscribe(clientListener) - } - - fun destroy() { - // makes sure to run - Log.d("${this.javaClass.simpleName}", "Destroy, Unsubscribe") - stop() - client.unsubscribe(clientListener) - bundler.cancel() - } - - open fun start() { - Log.d("${this.javaClass.simpleName}", "Start") - active = true - invalidateFilters() - } - - @OptIn(DelicateCoroutinesApi::class) - open fun stop() { - active = false - Log.d("${this.javaClass.simpleName}", "Stop") - - subscriptions.forEach { subscription -> - client.close(subscription.id) - subscription.reset() - } - } - - fun getSub(subId: String) = subscriptions.get(subId) - - fun requestNewSubscription(onEOSE: ((Long, String) -> Unit)? = null): Subscription = subscriptions.newSub(onEOSE) - - fun dismissSubscription(subId: String) { - getSub(subId)?.let { dismissSubscription(it) } - } - - fun dismissSubscription(subscription: Subscription) { - client.close(subscription.id) - subscription.reset() - subscriptions.remove(subscription) - } - - // Refreshes observers in batches. - private val bundler = BundledUpdate(300, Dispatchers.Default) - - fun invalidateFilters() { - bundler.invalidate { - // println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") - - // adds the time to perform the refresh into this delay - // holding off new updates in case of heavy refresh routines. - resetFiltersSuspend() - } - } - - suspend fun resetFiltersSuspend() { - // only runs one at a time. Ignores the others - if (changingFilters.compareAndSet(false, true)) { - Log.d("${this.javaClass.simpleName}", "resetFiltersSuspend $active") - try { - resetFiltersSuspendInner() - } finally { - changingFilters.getAndSet(false) - } - } - } - - private fun resetFiltersSuspendInner() { - // saves the channels that are currently active - val activeSubscriptions = subscriptions.actives() - // saves the current content to only update if it changes - val currentFilters = activeSubscriptions.associate { it.id to it.typedFilters } - - // updates all filters - updateSubscriptions() - - // Makes sure to only send an updated filter when it actually changes. - subscriptions.forEach { newSubscriptionFilters -> - val currentFilters = currentFilters[newSubscriptionFilters.id] - updateRelaysIfNeeded(newSubscriptionFilters, currentFilters) - } - } - - fun updateRelaysIfNeeded( - updatedSubscription: Subscription, - currentFilters: List?, - ) { - val updatedSubscriptionNewFilters = updatedSubscription.typedFilters - - val isActive = client.isActive(updatedSubscription.id) - - if (!isActive && updatedSubscriptionNewFilters != null) { - // Filter was removed from the active list - // but it is supposed to be there. Send again. - if (active) { - client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) - } - } else { - if (currentFilters != null) { - if (updatedSubscriptionNewFilters == null) { - // was active and is not active anymore, just close. - client.close(updatedSubscription.id) - } else { - // was active and is still active, check if it has changed. - if (updatedSubscription.hasChangedFiltersFrom(currentFilters)) { - client.close(updatedSubscription.id) - if (active) { - client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) - } - } else { - // hasn't changed, does nothing. - // unless the relay has disconnected, then reconnect. - if (active) { - client.sendFilterOnlyIfDisconnected(updatedSubscription.id, updatedSubscriptionNewFilters) - } - } - } - } else { - if (updatedSubscriptionNewFilters == null) { - // was not active and is still not active, does nothing - } else { - // was not active and becomes active, sends the filter. - if (active) { - client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) - } - } - } - } - } - - open fun consume( - event: Event, - relay: Relay, - ) = Unit - - open fun markAsSeenOnRelay( - eventId: String, - relay: Relay, - ) = Unit - - open fun markAsEOSE( - subscriptionId: String, - relay: Relay, - arrivalTime: Long, - ) { - subscriptions[subscriptionId]?.callEose( - arrivalTime, - relay.url, - ) - } - - open fun auth( - relay: Relay, - challenge: String, - ) = Unit - - open fun notify( - relay: Relay, - description: String, - ) = Unit - - abstract fun updateSubscriptions() -} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt index 91f56e7bcc..a5595470f0 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt @@ -20,116 +20,46 @@ */ package com.vitorpamplona.ammolite.relays.datasources -import com.vitorpamplona.ammolite.relays.TypedFilter -import com.vitorpamplona.ammolite.relays.filters.NormalFilter -import com.vitorpamplona.ammolite.relays.filters.SinceAuthorPerRelayFilter -import com.vitorpamplona.ammolite.relays.filters.SincePerRelayFilter -import java.util.UUID +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlin.collections.forEachIndexed data class Subscription( - val id: String = UUID.randomUUID().toString().substring(0, 4), - val onEose: ((time: Long, relayUrl: String) -> Unit)? = null, + val id: String = newSubId(), + val onEose: ((time: Long, relayUrl: NormalizedRelayUrl) -> Unit)? = null, ) { - var typedFilters: List? = null // Inactive when null + var relayBasedFilters: List? = null // Inactive when null fun reset() { - typedFilters = null + relayBasedFilters = null } fun callEose( time: Long, - relay: String, + relay: NormalizedRelayUrl, ) { onEose?.let { it(time, relay) } } - fun hasChangedFiltersFrom(otherFilters: List?): Boolean { - if (typedFilters == null && otherFilters == null) return false - if (typedFilters?.size != otherFilters?.size) return true + fun hasChangedFiltersFrom(otherFilters: List?): Boolean { + if (relayBasedFilters == null && otherFilters == null) return false + if (relayBasedFilters?.size != otherFilters?.size) return true - typedFilters?.forEachIndexed { index, typedFilter -> + relayBasedFilters?.forEachIndexed { index, relaySetFilter -> val otherFilter = otherFilters?.getOrNull(index) ?: return true - if (typedFilter.filter is SincePerRelayFilter && otherFilter.filter is SincePerRelayFilter) { - return isDifferent(typedFilter.filter, otherFilter.filter) - } + if (relaySetFilter.relay != otherFilter.relay) return true - if (typedFilter.filter is SinceAuthorPerRelayFilter && otherFilter.filter is SinceAuthorPerRelayFilter) { - return isDifferent(typedFilter.filter, otherFilter.filter) - } - - if (typedFilter.filter is NormalFilter && otherFilter.filter is NormalFilter) { - return isDifferent(typedFilter.filter, otherFilter.filter) - } - - return true + return isDifferent(relaySetFilter.filter, otherFilter.filter) } return false } fun isDifferent( - filter1: SincePerRelayFilter, - filter2: SincePerRelayFilter, - ): Boolean { - // Does not check SINCE on purpose. Avoids replacing the filter if SINCE was all that changed. - // fast check - if (filter1.authors?.size != filter2.authors?.size || - filter1.ids?.size != filter2.ids?.size || - filter1.tags?.size != filter2.tags?.size || - filter1.kinds?.size != filter2.kinds?.size || - filter1.limit != filter2.limit || - filter1.search?.length != filter2.search?.length || - filter1.until != filter2.until - ) { - return true - } - - // deep check - if (filter1.ids != filter2.ids || - filter1.authors != filter2.authors || - filter1.tags != filter2.tags || - filter1.kinds != filter2.kinds || - filter1.search != filter2.search - ) { - return true - } - - return false - } - - fun isDifferent( - filter1: SinceAuthorPerRelayFilter, - filter2: SinceAuthorPerRelayFilter, - ): Boolean { - // Does not check SINCE on purpose. Avoids replacing the filter if SINCE was all that changed. - // fast check - if (filter1.authors?.size != filter2.authors?.size || - filter1.ids?.size != filter2.ids?.size || - filter1.tags?.size != filter2.tags?.size || - filter1.kinds?.size != filter2.kinds?.size || - filter1.limit != filter2.limit || - filter1.search?.length != filter2.search?.length || - filter1.until != filter2.until - ) { - return true - } - - // deep check - if (filter1.ids != filter2.ids || - filter1.authors != filter2.authors || - filter1.tags != filter2.tags || - filter1.kinds != filter2.kinds || - filter1.search != filter2.search - ) { - return true - } - - return false - } - - fun isDifferent( - filter1: NormalFilter, - filter2: NormalFilter, + filter1: Filter, + filter2: Filter, ): Boolean { // Does not check SINCE on purpose. Avoids replacing the filter if SINCE was all that changed. // fast check diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt index daf7939174..5dd295a5dc 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt @@ -21,10 +21,12 @@ package com.vitorpamplona.ammolite.relays.datasources import com.vitorpamplona.ammolite.relays.BundledUpdate -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.Relay -import com.vitorpamplona.ammolite.relays.TypedFilter import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import java.util.concurrent.atomic.AtomicBoolean @@ -44,36 +46,36 @@ class SubscriptionController( val stats = SubscriptionStats() private val clientListener = - object : NostrClient.Listener { + object : IRelayClientListener { override fun onEvent( + relay: IRelayClient, + subId: String, event: Event, - subscriptionId: String, - relay: Relay, arrivalTime: Long, afterEOSE: Boolean, ) { - if (subscriptions.contains(subscriptionId)) { - stats.add(subscriptionId, event.kind) + if (subscriptions.contains(subId)) { + stats.add(subId, event.kind) if (afterEOSE) { - runAfterEOSE(subscriptionId, relay, arrivalTime) + runAfterEOSE(subId, relay, arrivalTime) } } } override fun onEOSE( - relay: Relay, - subscriptionId: String, + relay: IRelayClient, + subId: String, arrivalTime: Long, ) { - if (subscriptions.contains(subscriptionId)) { - runAfterEOSE(subscriptionId, relay, arrivalTime) + if (subscriptions.contains(subId)) { + runAfterEOSE(subId, relay, arrivalTime) } } } private fun runAfterEOSE( subscriptionId: String, - relay: Relay, + relay: IRelayClient, arrivalTime: Long, ) { subscriptions[subscriptionId]?.callEose(arrivalTime, relay.url) @@ -109,7 +111,7 @@ class SubscriptionController( fun getSub(subId: String) = subscriptions.get(subId) - fun requestNewSubscription(onEOSE: ((Long, String) -> Unit)? = null): Subscription = subscriptions.newSub(onEOSE) + fun requestNewSubscription(onEOSE: ((Long, NormalizedRelayUrl) -> Unit)? = null): Subscription = subscriptions.newSub(onEOSE) fun dismissSubscription(subId: String) = getSub(subId)?.let { dismissSubscription(it) } @@ -163,9 +165,9 @@ class SubscriptionController( fun updateRelaysIfNeeded( updatedSubscription: Subscription, - currentFilters: List?, + currentFilters: List?, ) { - val updatedSubscriptionNewFilters = updatedSubscription.typedFilters + val updatedSubscriptionNewFilters = updatedSubscription.relayBasedFilters val isActive = client.isActive(updatedSubscription.id) diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionSet.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionSet.kt index 404a8536f4..c40e3a42a2 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionSet.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionSet.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.ammolite.relays.datasources +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + class SubscriptionSet { private var subscriptions = mapOf() @@ -35,11 +37,11 @@ class SubscriptionSet { fun remove(sub: Subscription) = remove(sub.id) - fun newSub(onEOSE: ((Long, String) -> Unit)? = null): Subscription = Subscription(onEose = onEOSE).also { add(it) } + fun newSub(onEOSE: ((Long, NormalizedRelayUrl) -> Unit)? = null): Subscription = Subscription(onEose = onEOSE).also { add(it) } fun forEach(action: (Subscription) -> Unit) = subscriptions.values.forEach(action) operator fun get(subId: String) = subscriptions[subId] - fun actives() = subscriptions.values.filter { it.typedFilters != null } + fun actives() = subscriptions.values.filter { it.relayBasedFilters != null } } diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/EOSETime.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/MutableTime.kt similarity index 80% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/EOSETime.kt rename to ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/MutableTime.kt index f07ebed6ae..155f628c59 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/EOSETime.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/MutableTime.kt @@ -23,14 +23,25 @@ package com.vitorpamplona.ammolite.relays.filters /* * Wrapper class to allow changing in EOSE without modifying the list it is included within */ -class EOSETime( - var time: Long, -) { +class MutableTime(startTime: Long) { + var time: Long = startTime + private set + override fun toString(): String = time.toString() - fun update(newTime: Long) { + fun updateIfNewer(newTime: Long) { if (newTime > time) { time = newTime } } + + fun updateIfOlder(newTime: Long) { + if (newTime < time) { + time = newTime + } + } + + fun minus(delta: Int) = MutableTime(time - delta) + + fun copy() = MutableTime(time) } diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/NormalFilter.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/NormalFilter.kt deleted file mode 100644 index b837f826ca..0000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/NormalFilter.kt +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.relays.filters - -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterMatcher -import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterSerializer - -/** - * This is a nostr filter with per-relay authors list and since parameters - */ -class NormalFilter( - val ids: List? = null, - val authors: List? = null, - val kinds: List? = null, - val tags: Map>? = null, - val since: Long? = null, - val until: Long? = null, - val limit: Int? = null, - val search: String? = null, -) : IPerRelayFilter { - override fun isValidFor(url: String) = true - - override fun toRelay(forRelay: String) = Filter(ids, authors, kinds, tags, since, until, limit, search) - - override fun toJson(forRelay: String) = FilterSerializer.toJson(ids, authors, kinds, tags, since, until, limit, search) - - override fun match( - event: Event, - forRelay: String, - ) = FilterMatcher.match(event, ids, authors, kinds, tags, since, until) - - override fun toDebugJson(): String { - val obj = FilterSerializer.toJsonObject(ids, authors, kinds, tags, since, until, limit, search) - return EventMapper.toJson(obj) - } -} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SinceAuthorPerRelayFilter.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SinceAuthorPerRelayFilter.kt deleted file mode 100644 index 38abad5994..0000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SinceAuthorPerRelayFilter.kt +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.relays.filters - -import com.fasterxml.jackson.databind.node.JsonNodeFactory -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterMatcher -import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterSerializer - -/** - * This is a nostr filter with per-relay authors list and since parameters - */ -class SinceAuthorPerRelayFilter( - val ids: List? = null, - val authors: Map>? = null, - val kinds: List? = null, - val tags: Map>? = null, - val since: Map? = null, - val until: Long? = null, - val limit: Int? = null, - val search: String? = null, -) : IPerRelayFilter { - // This only exists because some relays consider empty arrays as null and return everything. - // So, if there is an author list, but no list for the specific relay or if the list is empty - // don't send it. - override fun isValidFor(forRelay: String) = authors == null || !authors[forRelay].isNullOrEmpty() - - override fun toRelay(forRelay: String) = Filter(ids, authors?.get(forRelay), kinds, tags, since?.get(forRelay)?.time, until, limit, search) - - override fun toJson(forRelay: String): String = FilterSerializer.toJson(ids, authors?.get(forRelay), kinds, tags, since?.get(forRelay)?.time, until, limit, search) - - override fun match( - event: Event, - forRelay: String, - ) = FilterMatcher.match(event, ids, authors?.get(forRelay), kinds, tags, since?.get(forRelay)?.time, until) - - override fun toDebugJson(): String { - val factory = JsonNodeFactory.instance - val obj = FilterSerializer.toJsonObject(ids, null, kinds, tags, null, until, limit, search) - authors?.run { - if (isNotEmpty()) { - val jsonObjectPerRelayAuthors = factory.objectNode() - entries.forEach { relayAuthorPairs -> - jsonObjectPerRelayAuthors.replace( - relayAuthorPairs.key, - factory.arrayNode(relayAuthorPairs.value.size).apply { - relayAuthorPairs.value.forEach { - add(it) - } - }, - ) - } - obj.replace("authors", jsonObjectPerRelayAuthors) - } - } - - since?.run { - if (isNotEmpty()) { - val jsonObjectSince = factory.objectNode() - entries.forEach { sincePairs -> - jsonObjectSince.put(sincePairs.key, "${sincePairs.value}") - } - obj.replace("since", jsonObjectSince) - } - } - return EventMapper.mapper.writeValueAsString(obj) - } -} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SincePerRelayFilter.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SincePerRelayFilter.kt deleted file mode 100644 index 522f368a3c..0000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/SincePerRelayFilter.kt +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.relays.filters - -import com.fasterxml.jackson.databind.node.JsonNodeFactory -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterMatcher -import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterSerializer - -/** - * This is a nostr filter with per-relay authors list and since parameters - */ -class SincePerRelayFilter( - val ids: List? = null, - val authors: List? = null, - val kinds: List? = null, - val tags: Map>? = null, - val since: Map? = null, - val until: Long? = null, - val limit: Int? = null, - val search: String? = null, -) : IPerRelayFilter { - override fun isValidFor(url: String) = true - - override fun toRelay(forRelay: String) = Filter(ids, authors, kinds, tags, since?.get(forRelay)?.time, until, limit, search) - - override fun toJson(forRelay: String) = FilterSerializer.toJson(ids, authors, kinds, tags, since?.get(forRelay)?.time, until, limit, search) - - override fun match( - event: Event, - forRelay: String, - ) = FilterMatcher.match(event, ids, authors, kinds, tags, since?.get(forRelay)?.time, until) - - override fun toDebugJson(): String { - val factory = JsonNodeFactory.instance - val obj = FilterSerializer.toJsonObject(ids, authors, kinds, tags, null, until, limit, search) - - since?.run { - if (isNotEmpty()) { - val jsonObjectSince = factory.objectNode() - entries.forEach { sincePairs -> - jsonObjectSince.put(sincePairs.key, "${sincePairs.value}") - } - obj.replace("since", jsonObjectSince) - } - } - return EventMapper.toJson(obj) - } -} diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt index 5afb1b72c9..0b8150e217 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt @@ -74,7 +74,7 @@ class HintIndexerBenchmark { val key = keys.random() benchmarkRule.measureRepeated { - indexer.getKey(key) + indexer.hintsForKey(key) } } diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt index 301a0180a9..cc26f55498 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt @@ -29,7 +29,7 @@ import java.util.regex.Pattern class Base64Image { companion object { - val pattern = Pattern.compile("data:image/(${RichTextParser.Companion.imageExtensions.joinToString(separator = "|") { it } });base64,([a-zA-Z0-9+/]+={0,2})") + val pattern = Pattern.compile("data:image/(${RichTextParser.imageExtensions.joinToString(separator = "|") { it } });base64,([a-zA-Z0-9+/]+={0,2})") fun isBase64(content: String): Boolean { val matcher = pattern.matcher(content) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 502b1926b8..258945d5e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] accompanistAdaptive = "0.37.3" activityCompose = "1.10.1" -agp = "8.10.0" +agp = "8.10.1" android-compileSdk = "35" android-minSdk = "26" android-targetSdk = "35" @@ -109,6 +109,7 @@ jna = { group = "net.java.dev.jna", name = "jna", version.ref = "jna" } jtorctl = { module = "info.guardianproject:jtorctl", version.ref = "jtorctl" } junit = { group = "junit", name = "junit", version.ref = "junit" } kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } kotlinx-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kotlinxSerialization" } lazysodium-android = { group = "com.goterl", name = "lazysodium-android", version.ref = "lazysodiumAndroid" } markdown-commonmark = { group = "com.github.vitorpamplona.compose-richtext", name = "richtext-commonmark", version.ref = "markdown" } diff --git a/quartz/build.gradle b/quartz/build.gradle index 12e9db7ed2..0a66790229 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -47,7 +47,6 @@ kotlin { dependencies { implementation libs.androidx.core.ktx - implementation platform(libs.androidx.compose.bom) // @Immutable and @Stable diff --git a/quartz/notebooks/Kind1Test.ipynb b/quartz/notebooks/Kind1Test.ipynb new file mode 100644 index 0000000000..707a981a83 --- /dev/null +++ b/quartz/notebooks/Kind1Test.ipynb @@ -0,0 +1,147 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Creating a new Kind 1 post" + }, + { + "metadata": { + "jupyter": { + "is_executing": true + } + }, + "cell_type": "code", + "source": [ + "USE {\n", + " dependencies(\"fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:0.17.3\")\n", + " dependencies(\"com.goterl:lazysodium-java:5.1.4\")\n", + " dependencies(\"net.java.dev.jna:jna:5.17.0\")\n", + "\n", + " import(\"com.vitorpamplona.quartz.*\")\n", + "}" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-06-03T14:42:40.051027Z", + "start_time": "2025-06-03T14:42:40.023052Z" + } + }, + "source": [ + "import fr.acinq.secp256k1.Secp256k1\n", + "\n", + "import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair\n", + "import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal\n", + "import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync\n", + "import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent\n", + "\n", + "val signer = NostrSignerSync()\n", + "val kind1Template = TextNoteEvent.build(\"New kind 1 Post\")\n", + "val signedEvent = signer.sign(kind1Template)\n", + "\n", + "println(signedEvent)" + ], + "outputs": [ + { + "ename": "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException", + "evalue": "at Cell In[5], line 3, column 43: Unresolved reference: crypto\nat Cell In[5], line 4, column 43: Unresolved reference: signers\nat Cell In[5], line 5, column 43: Unresolved reference: signers\nat Cell In[5], line 6, column 33: Unresolved reference: nip10Notes\nat Cell In[5], line 8, column 14: Unresolved reference: NostrSignerSync\nat Cell In[5], line 9, column 21: Unresolved reference: TextNoteEvent\nat Cell In[5], line 12, column 1: Overload resolution ambiguity: \npublic inline fun println(message: Any?): Unit defined in kotlin.io\npublic inline fun println(message: Boolean): Unit defined in kotlin.io\npublic inline fun println(message: Byte): Unit defined in kotlin.io\npublic inline fun println(message: Char): Unit defined in kotlin.io\npublic inline fun println(message: CharArray): Unit defined in kotlin.io\npublic inline fun println(message: Double): Unit defined in kotlin.io\npublic inline fun println(message: Float): Unit defined in kotlin.io\npublic inline fun println(message: Int): Unit defined in kotlin.io\npublic inline fun println(message: Long): Unit defined in kotlin.io\npublic inline fun println(message: Short): Unit defined in kotlin.io", + "output_type": "error", + "traceback": [ + "org.jetbrains.kotlinx.jupyter.exceptions.ReplCompilerException: at Cell In[5], line 3, column 43: Unresolved reference: crypto", + "at Cell In[5], line 4, column 43: Unresolved reference: signers", + "at Cell In[5], line 5, column 43: Unresolved reference: signers", + "at Cell In[5], line 6, column 33: Unresolved reference: nip10Notes", + "at Cell In[5], line 8, column 14: Unresolved reference: NostrSignerSync", + "at Cell In[5], line 9, column 21: Unresolved reference: TextNoteEvent", + "at Cell In[5], line 12, column 1: Overload resolution ambiguity: ", + "public inline fun println(message: Any?): Unit defined in kotlin.io", + "public inline fun println(message: Boolean): Unit defined in kotlin.io", + "public inline fun println(message: Byte): Unit defined in kotlin.io", + "public inline fun println(message: Char): Unit defined in kotlin.io", + "public inline fun println(message: CharArray): Unit defined in kotlin.io", + "public inline fun println(message: Double): Unit defined in kotlin.io", + "public inline fun println(message: Float): Unit defined in kotlin.io", + "public inline fun println(message: Int): Unit defined in kotlin.io", + "public inline fun println(message: Long): Unit defined in kotlin.io", + "public inline fun println(message: Short): Unit defined in kotlin.io", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.JupyterCompilerImpl.compileSync(JupyterCompilerImpl.kt:208)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.InternalEvaluatorImpl.eval(InternalEvaluatorImpl.kt:126)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl$execute$1$result$1.invoke(CellExecutorImpl.kt:80)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl$execute$1$result$1.invoke(CellExecutorImpl.kt:78)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.withHost(ReplForJupyterImpl.kt:791)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.CellExecutorImpl.execute-L4Nmkdk(CellExecutorImpl.kt:78)", + "\tat org.jetbrains.kotlinx.jupyter.repl.execution.CellExecutor$DefaultImpls.execute-L4Nmkdk$default(CellExecutor.kt:13)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.evaluateUserCode-wNURfNM(ReplForJupyterImpl.kt:613)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.evalExImpl(ReplForJupyterImpl.kt:471)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.access$evalExImpl(ReplForJupyterImpl.kt:143)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl$evalEx$1.invoke(ReplForJupyterImpl.kt:464)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl$evalEx$1.invoke(ReplForJupyterImpl.kt:463)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.withEvalContext(ReplForJupyterImpl.kt:444)", + "\tat org.jetbrains.kotlinx.jupyter.repl.impl.ReplForJupyterImpl.evalEx(ReplForJupyterImpl.kt:463)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1$1.invoke(IdeCompatibleMessageRequestProcessor.kt:159)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1$1.invoke(IdeCompatibleMessageRequestProcessor.kt:158)", + "\tat org.jetbrains.kotlinx.jupyter.streams.BlockingSubstitutionEngine.withDataSubstitution(SubstitutionEngine.kt:70)", + "\tat org.jetbrains.kotlinx.jupyter.streams.StreamSubstitutionManager.withSubstitutedStreams(StreamSubstitutionManager.kt:118)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.withForkedIn(IdeCompatibleMessageRequestProcessor.kt:335)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.access$withForkedIn(IdeCompatibleMessageRequestProcessor.kt:54)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$evalWithIO$1$1.invoke(IdeCompatibleMessageRequestProcessor.kt:349)", + "\tat org.jetbrains.kotlinx.jupyter.streams.BlockingSubstitutionEngine.withDataSubstitution(SubstitutionEngine.kt:70)", + "\tat org.jetbrains.kotlinx.jupyter.streams.StreamSubstitutionManager.withSubstitutedStreams(StreamSubstitutionManager.kt:118)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.withForkedErr(IdeCompatibleMessageRequestProcessor.kt:324)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.access$withForkedErr(IdeCompatibleMessageRequestProcessor.kt:54)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$evalWithIO$1.invoke(IdeCompatibleMessageRequestProcessor.kt:348)", + "\tat org.jetbrains.kotlinx.jupyter.streams.BlockingSubstitutionEngine.withDataSubstitution(SubstitutionEngine.kt:70)", + "\tat org.jetbrains.kotlinx.jupyter.streams.StreamSubstitutionManager.withSubstitutedStreams(StreamSubstitutionManager.kt:118)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.withForkedOut(IdeCompatibleMessageRequestProcessor.kt:316)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor.evalWithIO(IdeCompatibleMessageRequestProcessor.kt:347)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1.invoke(IdeCompatibleMessageRequestProcessor.kt:158)", + "\tat org.jetbrains.kotlinx.jupyter.messaging.IdeCompatibleMessageRequestProcessor$processExecuteRequest$1$response$1.invoke(IdeCompatibleMessageRequestProcessor.kt:157)", + "\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl$Task.execute(JupyterExecutorImpl.kt:41)", + "\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl$executorThread$1.invoke(JupyterExecutorImpl.kt:83)", + "\tat org.jetbrains.kotlinx.jupyter.execution.JupyterExecutorImpl$executorThread$1.invoke(JupyterExecutorImpl.kt:80)", + "\tat kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)", + "" + ] + } + ], + "execution_count": 5 + }, + { + "metadata": {}, + "cell_type": "code", + "source": "", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "name": "kotlin", + "version": "1.9.23", + "mimetype": "text/x-kotlin", + "file_extension": ".kt", + "pygments_lexer": "kotlin", + "codemirror_mode": "text/x-kotlin", + "nbconvert_exporter": "" + }, + "ktnbPluginMetadata": { + "projectDependencies": [ + "Amethyst.amethyst.unitTest" + ], + "projectLibraries": false + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt index 66c0d41d51..be92b41be2 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt @@ -51,14 +51,14 @@ class OtsTest { @Test fun verifyNostrEvent2() { - val ots = Event.Companion.fromJson(otsEvent2) as OtsEvent + val ots = Event.fromJson(otsEvent2) as OtsEvent println(resolver.info(ots.otsByteArray())) assertEquals(1706322179L, ots.verify(resolver)) } @Test fun verifyNostrPendingEvent() { - val ots = Event.Companion.fromJson(otsPendingEvent) as OtsEvent + val ots = Event.fromJson(otsPendingEvent) as OtsEvent println(resolver.info(ots.otsByteArray())) assertEquals(null, ots.verify(resolver)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 17540a0507..6fd03794e0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.quartz -import com.vitorpamplona.quartz.blossom.BlossomAuthorizationEvent -import com.vitorpamplona.quartz.blossom.BlossomServersEvent import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent @@ -82,6 +80,8 @@ import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent +import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent @@ -102,9 +102,9 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent -import com.vitorpamplona.quartz.nip72ModCommunities.CommunityListEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent @@ -119,6 +119,8 @@ import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent class EventFactory { companion object { @@ -205,12 +207,14 @@ class EventFactory { FhirResourceEvent.KIND -> FhirResourceEvent(id, pubKey, createdAt, tags, content, sig) FollowListEvent.KIND -> FollowListEvent(id, pubKey, createdAt, tags, content, sig) GenericRepostEvent.KIND -> GenericRepostEvent(id, pubKey, createdAt, tags, content, sig) + GeohashListEvent.KIND -> GeohashListEvent(id, pubKey, createdAt, tags, content, sig) GiftWrapEvent.KIND -> GiftWrapEvent(id, pubKey, createdAt, tags, content, sig) GitIssueEvent.KIND -> GitIssueEvent(id, pubKey, createdAt, tags, content, sig) GitReplyEvent.KIND -> GitReplyEvent(id, pubKey, createdAt, tags, content, sig) GitPatchEvent.KIND -> GitPatchEvent(id, pubKey, createdAt, tags, content, sig) GitRepositoryEvent.KIND -> GitRepositoryEvent(id, pubKey, createdAt, tags, content, sig) GoalEvent.KIND -> GoalEvent(id, pubKey, createdAt, tags, content, sig) + HashtagListEvent.KIND -> HashtagListEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) InteractiveStoryPrologueEvent.KIND -> InteractiveStoryPrologueEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/ParticipantTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/ParticipantTag.kt index eada7c6458..3f7e65b260 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/ParticipantTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/track/tags/ParticipantTag.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.ensure @@ -31,7 +33,7 @@ import com.vitorpamplona.quartz.utils.ensure @Immutable data class ParticipantTag( override val pubKey: String, - override val relayHint: String?, + override val relayHint: NormalizedRelayUrl?, ) : PubKeyReferenceTag { fun toTagArray() = assemble(pubKey, relayHint) @@ -43,7 +45,7 @@ data class ParticipantTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return ParticipantTag(tag[1], tag.getOrNull(2)) + return ParticipantTag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }) } @JvmStatic @@ -57,7 +59,7 @@ data class ParticipantTag( @JvmStatic fun assemble( pubkey: HexKey, - relayHint: String? = null, - ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint) + relayHint: NormalizedRelayUrl? = null, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint?.url) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt index 6b2e63d48a..fe6ce61b9d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt @@ -23,10 +23,12 @@ package com.vitorpamplona.quartz.experimental.edits import android.util.Log import androidx.compose.runtime.Immutable import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.experimental.edits.tags.RelayTag import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -53,22 +55,11 @@ class PrivateOutboxRelayListEvent( super.countMemory() + pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0) - fun relays(): List? = + fun relays(): List? = tags - .mapNotNull { - if (it.size > 1 && it[0] == "relay") { - it[1] - } else { - null - } - }.plus( - privateTagsCache?.mapNotNull { - if (it.size > 1 && it[0] == "relay") { - it[1] - } else { - null - } - } ?: emptyList(), + .mapNotNull(RelayTag::parse) + .plus( + privateTagsCache?.mapNotNull(RelayTag::parse) ?: emptyList(), ).ifEmpty { null } fun cachedPrivateTags(): Array>? = privateTagsCache @@ -125,25 +116,25 @@ class PrivateOutboxRelayListEvent( ) } - fun createTagArray(relays: List): Array> = + fun createTagArray(relays: List): Array> = relays .map { - arrayOf("relay", it) + RelayTag.assemble(it) }.toTypedArray() fun updateRelayList( earlierVersion: PrivateOutboxRelayListEvent, - relays: List, + relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PrivateOutboxRelayListEvent) -> Unit, ) { val tags = earlierVersion.privateTagsCache - ?.filter { it[0] != "relay" } + ?.filter(RelayTag::notMatch) ?.plus( relays.map { - arrayOf("relay", it) + RelayTag.assemble(it) }, )?.toTypedArray() ?: emptyArray() @@ -156,7 +147,7 @@ class PrivateOutboxRelayListEvent( } fun createFromScratch( - relays: List, + relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PrivateOutboxRelayListEvent) -> Unit, @@ -165,7 +156,7 @@ class PrivateOutboxRelayListEvent( } fun create( - relays: List, + relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (PrivateOutboxRelayListEvent) -> Unit, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/tags/RelayTag.kt new file mode 100644 index 0000000000..c69057fd1c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/tags/RelayTag.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.edits.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.ensure + +class RelayTag { + companion object { + const val TAG_NAME = "relay" + + @JvmStatic + fun match(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun notMatch(tag: Array) = tag.has(0) && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): NormalizedRelayUrl? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null + + return relay + } + + @JvmStatic + fun assemble(relay: NormalizedRelayUrl) = arrayOf(TAG_NAME, relay.url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/EphemeralChatEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/EphemeralChatEvent.kt index fff181bf43..593a2b5562 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/EphemeralChatEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/EphemeralChatEvent.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.tags.RoomTag import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @@ -39,11 +40,13 @@ class EphemeralChatEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun hasHomeRelay() = tags.any(RelayTag::match) + fun room() = tags.firstNotNullOfOrNull(RoomTag::parse) ?: DEFAULT_ROOM - fun relay() = tags.firstNotNullOfOrNull(RelayTag::parse) ?: "" + fun relay() = tags.firstNotNullOfOrNull(RelayTag::parse) - fun roomId() = RoomId(room(), relay()) + fun roomId() = relay()?.let { RoomId(room(), it) } companion object { const val KIND = 23333 @@ -53,7 +56,7 @@ class EphemeralChatEvent( fun build( message: String, - relay: String, + relay: NormalizedRelayUrl, room: String = DEFAULT_ROOM, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt index c515febfd4..d249be3516 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt @@ -20,13 +20,14 @@ */ package com.vitorpamplona.quartz.experimental.ephemChat.chat -import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl data class RoomId( val id: String, - val relayUrl: String, + val relayUrl: NormalizedRelayUrl, ) { fun toKey() = "$id@$relayUrl" - fun toDisplayKey() = id + "@" + RelayUrlFormatter.displayUrl(relayUrl) + fun toDisplayKey() = id + "@" + relayUrl.displayUrl() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/TagArrayBuilderExt.kt index 62c04bc099..1b5978f30a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/TagArrayBuilderExt.kt @@ -23,7 +23,8 @@ package com.vitorpamplona.quartz.experimental.ephemChat.chat import com.vitorpamplona.quartz.experimental.ephemChat.chat.tags.RelayTag import com.vitorpamplona.quartz.experimental.ephemChat.chat.tags.RoomTag import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl fun TagArrayBuilder.room(room: String) = addUnique(RoomTag.assemble(room)) -fun TagArrayBuilder.relay(room: String) = addUnique(RelayTag.assemble(room)) +fun TagArrayBuilder.relay(relay: NormalizedRelayUrl) = addUnique(RelayTag.assemble(relay)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt index 65baca74af..4f596e31ec 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt @@ -20,7 +20,10 @@ */ package com.vitorpamplona.quartz.experimental.ephemChat.chat.tags +import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.ensure class RelayTag { @@ -28,14 +31,18 @@ class RelayTag { const val TAG_NAME = "relay" @JvmStatic - fun parse(tag: Array): String? { + fun match(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun parse(tag: Array): NormalizedRelayUrl? { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - return tag[1] + + return RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null } @JvmStatic - fun assemble(url: String) = arrayOf(TAG_NAME, url) + fun assemble(relay: NormalizedRelayUrl) = arrayOf(TAG_NAME, relay.url) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RoomTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RoomTag.kt index 24fef9b5b9..e97ea19d96 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RoomTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RoomTag.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.experimental.ephemChat.chat.tags +import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.ensure @@ -27,6 +28,9 @@ class RoomTag { companion object { const val TAG_NAME = "d" + @JvmStatic + fun match(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + @JvmStatic fun parse(tag: Array): String? { ensure(tag.has(1)) { return null } @@ -36,6 +40,6 @@ class RoomTag { } @JvmStatic - fun assemble(url: String) = arrayOf(TAG_NAME, url) + fun assemble(room: String) = arrayOf(TAG_NAME, room) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/db/EphemeralRoom.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/db/EphemeralRoom.kt index 1d4674b7f5..5d687851d9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/db/EphemeralRoom.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/db/EphemeralRoom.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.LargeCache import kotlinx.coroutines.flow.MutableStateFlow @@ -31,7 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class Room( val roomId: RoomId, ) { - constructor(id: String, relayUrl: String) : this(RoomId(id, relayUrl)) + constructor(id: String, relayUrl: NormalizedRelayUrl) : this(RoomId(id, relayUrl)) val messages = LargeCache() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/db/EphemeralRoomCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/db/EphemeralRoomCache.kt index e2b7cb0dd1..2785203e06 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/db/EphemeralRoomCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/db/EphemeralRoomCache.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.experimental.ephemChat.db import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.LargeCache class EphemeralRoomCache { @@ -28,12 +29,12 @@ class EphemeralRoomCache { fun getRoomIfExists( roomId: String, - relayUrl: String, + relayUrl: NormalizedRelayUrl, ): Room? = rooms.get(RoomId(roomId, relayUrl)) fun getOrCreateRoom( roomId: String, - relayUrl: String, + relayUrl: NormalizedRelayUrl, ): Room = rooms.getOrCreate(RoomId(roomId, relayUrl)) { Room(roomId, relayUrl) } fun findRoomsStartingWith(text: String): List { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt index d09b9c647c..99ac56aa99 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt @@ -81,9 +81,9 @@ class EphemeralChatListEvent( createdAt: Long = TimeUtils.now(), onReady: (EphemeralChatListEvent) -> Unit, ) { - val tags = arrayOf(RoomIdTag.Companion.assemble(room)) + val tags = arrayOf(RoomIdTag.assemble(room)) if (isPrivate) { - PrivateTagsInContent.Companion.encryptNip04( + PrivateTagsInContent.encryptNip04( privateTags = tags, signer = signer, ) { encryptedTags -> @@ -104,7 +104,7 @@ class EphemeralChatListEvent( ) { PrivateTagArrayBuilder.removeAll( earlierVersion, - RoomIdTag.Companion.assemble(room.id, room.relayUrl), + RoomIdTag.assemble(room.id, room.relayUrl), signer, ) { encryptedContent, newTags -> create(encryptedContent, newTags, signer, createdAt, onReady) @@ -121,7 +121,7 @@ class EphemeralChatListEvent( ) { PrivateTagArrayBuilder.add( earlierVersion, - RoomIdTag.Companion.assemble(room.id, room.relayUrl), + RoomIdTag.assemble(room.id, room.relayUrl.url), isPrivate, signer, ) { encryptedContent, newTags -> @@ -140,7 +140,7 @@ class EphemeralChatListEvent( if (tags.any { it.size > 1 && it[0] == "alt" }) { tags } else { - tags + AltTag.Companion.assemble(ALT) + tags + AltTag.assemble(ALT) } signer.sign(createdAt, KIND, newTags, content, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/tags/RoomIdTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/tags/RoomIdTag.kt index d8f569a138..382fd64d9a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/tags/RoomIdTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/tags/RoomIdTag.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.quartz.experimental.ephemChat.list.tags import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.ensure class RoomIdTag { @@ -30,11 +32,13 @@ class RoomIdTag { @JvmStatic fun parse(tag: Array): RoomId? { - ensure(tag.has(1)) { return null } + ensure(tag.has(2)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } ensure(tag[2].isNotEmpty()) { return null } - return RoomId(tag[1], tag[2]) + + val relay = RelayUrlNormalizer.normalizeOrNull(tag[2]) ?: return null + return RoomId(tag[1], relay) } @JvmStatic @@ -44,6 +48,12 @@ class RoomIdTag { ) = arrayOf(TAG_NAME, id, relayUrl) @JvmStatic - fun assemble(id: RoomId) = arrayOf(TAG_NAME, id.id, id.relayUrl) + fun assemble( + id: String, + relay: NormalizedRelayUrl, + ) = assemble(id, relay.url) + + @JvmStatic + fun assemble(id: RoomId) = arrayOf(TAG_NAME, id.id, id.relayUrl.url) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt index 863a021b28..cc2971896f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.experimental.forks +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.MARKER @@ -29,8 +30,8 @@ fun MarkedETag.Companion.parseFork(tag: Array): MarkedETag? { // ["e", id hex, relay hint, marker, pubkey] return MarkedETag( tag[ORDER_EVT_ID], - tag[ORDER_RELAY], - tag[ORDER_MARKER], + tag[ORDER_RELAY].ifBlank { null }?.let { RelayUrlNormalizer.normalizeOrNull(it) }, + MARKER.FORK, tag.getOrNull( ORDER_PUBKEY, ), diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryReadingStateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryReadingStateEvent.kt index 83ac71bb4f..1d5725f0fc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryReadingStateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/InteractiveStoryReadingStateEvent.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.builder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag @@ -83,7 +84,7 @@ class InteractiveStoryReadingStateEvent( fun update( base: InteractiveStoryReadingStateEvent, currentScene: InteractiveStoryBaseEvent, - currentSceneRelay: String?, + currentSceneRelay: NormalizedRelayUrl?, createdAt: Long = TimeUtils.now(), ): EventTemplate { val rootTag = base.dTag() @@ -109,9 +110,9 @@ class InteractiveStoryReadingStateEvent( fun build( root: InteractiveStoryBaseEvent, - rootRelay: String?, + rootRelay: NormalizedRelayUrl?, currentScene: InteractiveStoryBaseEvent, - currentSceneRelay: String?, + currentSceneRelay: NormalizedRelayUrl?, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt index 0a1054f5dd..ab50b9071f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt @@ -23,12 +23,13 @@ package com.vitorpamplona.quartz.experimental.interactiveStories.tags import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure import com.vitorpamplona.quartz.utils.pointerSizeInBytes -import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers @Immutable data class RootSceneTag( @@ -36,13 +37,13 @@ data class RootSceneTag( val pubKeyHex: String, val dTag: String, ) { - var relay: String? = null + var relay: NormalizedRelayUrl? = null constructor( kind: Int, pubKeyHex: HexKey, dTag: String, - relayHint: String?, + relayHint: NormalizedRelayUrl?, ) : this(kind, pubKeyHex, dTag) { this.relay = relayHint } @@ -52,11 +53,11 @@ data class RootSceneTag( 8L + // kind pubKeyHex.bytesUsedInMemory() + dTag.bytesUsedInMemory() + - (relay?.bytesUsedInMemory() ?: 0) + (relay?.url?.bytesUsedInMemory() ?: 0) fun toTag() = assembleATagId(kind, pubKeyHex, dTag) - fun toTagArray() = removeTrailingNullsAndEmptyOthers(TAG_NAME, toTag(), relay) + fun toTagArray() = assemble(kind, pubKeyHex, dTag, relay) companion object { const val TAG_NAME = "A" @@ -73,21 +74,22 @@ data class RootSceneTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } val address = Address.parse(tag[1]) ?: return null - return RootSceneTag(address.kind, address.pubKeyHex, address.dTag, tag.getOrNull(2)) + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + return RootSceneTag(address.kind, address.pubKeyHex, address.dTag, hint) } @JvmStatic fun assemble( aTagId: HexKey, - relay: String?, - ) = arrayOfNotNull(TAG_NAME, aTagId, relay) + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url) @JvmStatic fun assemble( kind: Int, pubKeyHex: String, dTag: String, - relay: String?, - ) = arrayOfNotNull(TAG_NAME, assembleATagId(kind, pubKeyHex, dTag), relay) + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, assembleATagId(kind, pubKeyHex, dTag), relay?.url) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/medical/FhirResourceEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/medical/FhirResourceEvent.kt index f9082bd77a..57577ee37e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/medical/FhirResourceEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/medical/FhirResourceEvent.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.experimental.medical import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent.Companion.ALT import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder @@ -47,7 +46,7 @@ class FhirResourceEvent( createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, fhirPayload, createdAt) { - alt(ALT) + alt(ALT_DESCRIPTION) initializer() } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt index 7ea3a92bbc..56bcfc9aeb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/TagArrayBuilderExt.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.experimental.profileGallery import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag @@ -68,5 +69,5 @@ fun TagArrayBuilder.service(service: String) = add(Ser fun TagArrayBuilder.fromEvent( event: HexKey, - relayHint: String?, + relayHint: NormalizedRelayUrl?, ) = add(ETag.assemble(event, relayHint, null)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/AddressableEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/AddressableEvent.kt index 914b90bb3e..c59f5b0192 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/AddressableEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/AddressableEvent.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.core import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -28,7 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address interface AddressableEvent : IEvent { fun dTag(): String - fun aTag(relayHint: String? = null): ATag + fun aTag(relayHint: NormalizedRelayUrl? = null): ATag fun address(): Address diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseAddressableEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseAddressableEvent.kt index b2727bb2e7..cf857f66c8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseAddressableEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseAddressableEvent.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.core import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag @@ -38,7 +39,7 @@ open class BaseAddressableEvent( AddressableEvent { override fun dTag() = tags.dTag() - override fun aTag(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint) + override fun aTag(relayHint: NormalizedRelayUrl?) = ATag(kind, pubKey, dTag(), relayHint) override fun address() = Address(kind, pubKey, dTag()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseReplaceableEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseReplaceableEvent.kt index c8632b9a5d..8f520077bc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseReplaceableEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/BaseReplaceableEvent.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.core import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -36,7 +37,7 @@ open class BaseReplaceableEvent( ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { override fun dTag() = FIXED_D_TAG - override fun aTag(relayHint: String?) = ATag(kind, pubKey, FIXED_D_TAG, relayHint) + override fun aTag(relayHint: NormalizedRelayUrl?) = ATag(kind, pubKey, FIXED_D_TAG, relayHint) override fun address() = Address(kind, pubKey, dTag()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt index c5bbd6e1a3..56961814c2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt @@ -22,11 +22,13 @@ package com.vitorpamplona.quartz.nip01Core.hints import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -35,10 +37,10 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes data class EventHintBundle( val event: T, ) { - var relay: String? = null - var authorHomeRelay: String? = null + var relay: NormalizedRelayUrl? = null + var authorHomeRelay: NormalizedRelayUrl? = null - constructor(event: T, relayHint: String? = null, authorHomeRelay: String? = null) : this(event) { + constructor(event: T, relayHint: NormalizedRelayUrl? = null, authorHomeRelay: NormalizedRelayUrl? = null) : this(event) { this.relay = relayHint this.authorHomeRelay = authorHomeRelay } @@ -46,7 +48,7 @@ data class EventHintBundle( fun countMemory(): Long = 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) event.countMemory() + - (relay?.bytesUsedInMemory() ?: 0) + (relay?.url?.bytesUsedInMemory() ?: 0) fun toNEvent(): String = NEvent.create(event.id, event.pubKey, event.kind, relay) @@ -60,5 +62,5 @@ data class EventHintBundle( fun toETagArray() = ETag.assemble(event.id, relay, event.pubKey) - fun toQTagArray() = ETag(event.id, relay, event.pubKey).toQTagArray() + fun toQTagArray() = QEventTag.assemble(event.id, relay, event.pubKey) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt index 351de15ff3..d6d498525e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip01Core.hints import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.hints.bloom.BloomFilterMurMur3 +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.LargeCache /** * Instead of having one bloom filter per relay per type, which could create @@ -34,70 +36,70 @@ class HintIndexer { private val eventHints = BloomFilterMurMur3(10_000_000, 5) private val addressHints = BloomFilterMurMur3(2_000_000, 5) private val pubKeyHints = BloomFilterMurMur3(10_000_000, 5) - private val relayDB = mutableSetOf() + private val relayDB = LargeCache() private fun add( id: ByteArray, - relay: String, + relay: NormalizedRelayUrl, bloom: BloomFilterMurMur3, ) { - relayDB.add(relay) + relayDB.put(relay, relay) bloom.add(id, relay.hashCode()) } - private fun get( + private fun getHintsFor( id: ByteArray, bloom: BloomFilterMurMur3, - ) = relayDB.filter { bloom.mightContain(id, it.hashCode()) } + ) = relayDB.filter { relay, _ -> bloom.mightContain(id, relay.hashCode()) } // -------------------- // Event Host hints // -------------------- fun addEvent( eventId: ByteArray, - relay: String, + relay: NormalizedRelayUrl, ) = add(eventId, relay, eventHints) fun addEvent( eventId: HexKey, - relay: String, + relay: NormalizedRelayUrl, ) = addEvent(eventId.hexToByteArray(), relay) - fun getEvent(eventId: ByteArray) = get(eventId, eventHints) + fun hintsForEvent(eventId: ByteArray) = getHintsFor(eventId, eventHints) - fun getEvent(eventId: HexKey) = getEvent(eventId.hexToByteArray()) + fun hintsForEvent(eventId: HexKey) = hintsForEvent(eventId.hexToByteArray()) // -------------------- // PubKeys Outbox hints // -------------------- fun addAddress( addressId: ByteArray, - relay: String, + relay: NormalizedRelayUrl, ) = add(addressId, relay, addressHints) fun addAddress( addressId: String, - relay: String, + relay: NormalizedRelayUrl, ) = addAddress(addressId.toByteArray(), relay) - fun getAddress(addressId: ByteArray) = get(addressId, addressHints) + fun hintsForAddress(addressId: ByteArray) = getHintsFor(addressId, addressHints) - fun getAddress(addressId: String) = getAddress(addressId.toByteArray()) + fun hintsForAddress(addressId: String) = hintsForAddress(addressId.toByteArray()) // -------------------- // PubKeys Outbox hints // -------------------- fun addKey( key: ByteArray, - relay: String, + relay: NormalizedRelayUrl, ) = add(key, relay, pubKeyHints) fun addKey( key: HexKey, - relay: String, + relay: NormalizedRelayUrl, ) = addKey(key.hexToByteArray(), relay) - fun getKey(key: ByteArray) = get(key, pubKeyHints) + fun hintsForKey(key: ByteArray) = getHintsFor(key, pubKeyHints) - fun getKey(key: HexKey) = getKey(key.hexToByteArray()) + fun hintsForKey(key: HexKey) = hintsForKey(key.hexToByteArray()) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/AddressHint.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/AddressHint.kt index 26c8f35c50..9292790a55 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/AddressHint.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/AddressHint.kt @@ -20,9 +20,11 @@ */ package com.vitorpamplona.quartz.nip01Core.hints.types +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + data class AddressHint( val addressId: String, - var relay: String? = null, + val relay: NormalizedRelayUrl, ) : Hint { override fun id() = addressId.toByteArray(Charsets.UTF_8) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/EventIdHint.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/EventIdHint.kt index 88015fe0f8..556cb221b4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/EventIdHint.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/EventIdHint.kt @@ -22,10 +22,18 @@ package com.vitorpamplona.quartz.nip01Core.hints.types import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl data class EventIdHint( val eventId: HexKey, - var relay: String? = null, + val relay: NormalizedRelayUrl, +) : Hint { + override fun id() = eventId.hexToByteArray() +} + +data class EventIdHintOptional( + val eventId: HexKey, + val relay: NormalizedRelayUrl? = null, ) : Hint { override fun id() = eventId.hexToByteArray() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/PubKeyHint.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/PubKeyHint.kt index a8040bb516..f514f68a49 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/PubKeyHint.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/types/PubKeyHint.kt @@ -22,10 +22,11 @@ package com.vitorpamplona.quartz.nip01Core.hints.types import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl data class PubKeyHint( val pubkey: HexKey, - var relay: String? = null, + val relay: NormalizedRelayUrl, ) : Hint { override fun id() = pubkey.hexToByteArray() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt new file mode 100644 index 0000000000..ce1eeca333 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -0,0 +1,276 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client + +import android.util.Log +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.PoolEventOutboxRepository +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.PoolSubscriptionRepository +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.forEach +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +/** + * The Nostr Client manages a relay pool, keeps active subscriptions and manages sending of events. + */ +class NostrClient( + private val websocketBuilder: WebsocketBuilder, + private val scope: CoroutineScope, +) : IRelayClientListener { + private val relayPool: RelayPool = RelayPool(this, ::buildRelay) + private val activeSubscriptions: PoolSubscriptionRepository = PoolSubscriptionRepository() + private val eventOutbox: PoolEventOutboxRepository = PoolEventOutboxRepository() + + private var listeners = setOf() + + /** + * Whatches for any changes in the relay list from subscriptions or outbox + * and updates the relayPool as needed. + */ + private val allRelays = + combine( + activeSubscriptions.relays, + eventOutbox.relays, + ) { subs, outbox -> + subs + outbox + }.onStart { + activeSubscriptions.relays.value + eventOutbox.relays.value + }.onEach { + relayPool.updatePool(it) + + val subs = activeSubscriptions.allSubscriptions() + + it.forEach { relay -> + var hasSub = false + subs.forEach { sub -> + sub.value.filters.forEach { + if (it.isValidFor(relay)) { + hasSub = true + } + } + } + if (!hasSub) { + Log.d("NostrClient", "AABBCC $relay doesn't have any sub") + } + + if (relayPool.getRelay(relay)?.isConnected() == false) { + Log.d("NostrClient", "AABBCC $relay is not connected") + } + } + + val filters = subs.values.sumOf { it.filters.size } + Log.d("NostrClient", "Updating list for relays to ${it.size} relays and ${subs.size} subscriptions and $filters filters") + it.forEach { + Log.d("NostrClient", "${it.url} isConnected ${relayPool.getRelay(it)?.isConnected()}") + } + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Companion.Eagerly, + activeSubscriptions.relays.value + eventOutbox.relays.value, + ) + + fun buildRelay(relay: NormalizedRelayUrl): IRelayClient = + BasicRelayClient( + url = relay, + socketBuilder = websocketBuilder, + listener = relayPool, + stats = RelayStats.get(relay), + ) { liveRelay -> + activeSubscriptions.forEachSub(relay, liveRelay::sendRequest) + eventOutbox.forEachUnsentEvent(relay, liveRelay::send) + } + + fun connectedRelayList() = relayPool.getAll() + + // Reconnects all relays that may have disconnected + fun connect() = relayPool.connect() + + // Reconnects all relays that may have disconnected + fun disconnect() = relayPool.disconnect() + + @Synchronized + fun reconnect(onlyIfChanged: Boolean = false) { + if (onlyIfChanged) { + relayPool.getAllNeedsToReconnect().forEach { + it.disconnect() + } + relayPool.connect() + } else { + relayPool.disconnect() + relayPool.connect() + } + } + + fun sendFilter( + subscriptionId: String = newSubId(), + filters: List = listOf(), + ) { + filters.forEach { + Log.d("NostrClient", "${it.relay.url} ${it.filter.toJson()}") + } + + activeSubscriptions.addOrUpdate(subscriptionId, filters) + relayPool.sendRequest(subscriptionId, filters) + } + + fun sendFilterOnlyIfDisconnected( + subscriptionId: String = newSubId(), + filters: List = listOf(), + ) { + activeSubscriptions.addOrUpdate(subscriptionId, filters) + relayPool.connectIfDisconnected() + } + + fun sendIfExists( + signedEvent: Event, + connectedRelay: NormalizedRelayUrl, + ) { + relayPool.getRelay(connectedRelay)?.send(signedEvent) + } + + fun send( + signedEvent: Event, + relayList: Set, + ) { + eventOutbox.markAsSending(signedEvent, relayList) + relayPool.send(signedEvent, relayList) + } + + fun close(subscriptionId: String) { + relayPool.close(subscriptionId) + activeSubscriptions.remove(subscriptionId) + } + + fun isActive(subscriptionId: String): Boolean = activeSubscriptions.isActive(subscriptionId) + + override fun onEvent( + relay: IRelayClient, + subId: String, + event: Event, + arrivalTime: Long, + afterEOSE: Boolean, + ) { + listeners.forEach { it.onEvent(relay, subId, event, arrivalTime, afterEOSE) } + } + + override fun onEOSE( + relay: IRelayClient, + subId: String, + arrivalTime: Long, + ) { + listeners.forEach { it.onEOSE(relay, subId, arrivalTime) } + } + + override fun onRelayStateChange( + relay: IRelayClient, + type: RelayState, + ) { + listeners.forEach { it.onRelayStateChange(relay, type) } + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onBeforeSend( + relay: IRelayClient, + event: Event, + ) { + eventOutbox.newTry(event.id, relay.url) + listeners.forEach { it.onBeforeSend(relay, event) } + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onSendResponse( + relay: IRelayClient, + eventId: String, + success: Boolean, + message: String, + ) { + eventOutbox.newResponse(eventId, relay.url, success, message) + listeners.forEach { it.onSendResponse(relay, eventId, success, message) } + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onAuth( + relay: IRelayClient, + challenge: String, + ) { + listeners.forEach { it.onAuth(relay, challenge) } + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onNotify( + relay: IRelayClient, + description: String, + ) { + listeners.forEach { it.onNotify(relay, description) } + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onSend( + relay: IRelayClient, + msg: String, + success: Boolean, + ) { + listeners.forEach { it.onSend(relay, msg, success) } + } + + @OptIn(DelicateCoroutinesApi::class) + override fun onError( + relay: IRelayClient, + subId: String, + error: Error, + ) { + listeners.forEach { it.onError(relay, subId, error) } + } + + fun subscribe(listener: IRelayClientListener) { + listeners = listeners.plus(listener) + } + + fun isSubscribed(listener: IRelayClientListener): Boolean = listeners.contains(listener) + + fun unsubscribe(listener: IRelayClientListener) { + listeners = listeners.minus(listener) + } + + fun getSubscriptionFiltersOrNull(subId: String): List? = activeSubscriptions.getSubscriptionFiltersOrNull(subId) + + fun connectedRelays() = relayPool.connectedRelays() + + fun relayStatusFlow() = relayPool.statusFlow +} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/EventCollector.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/EventCollector.kt similarity index 80% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/EventCollector.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/EventCollector.kt index 156873ac5d..42d63f53a5 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/EventCollector.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/EventCollector.kt @@ -18,26 +18,27 @@ * 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.ammolite.relays.datasources +package com.vitorpamplona.quartz.nip01Core.relay.client.acessories import android.util.Log -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient /** * Listens to NostrClient's onEvent messages for caching purposes. */ class EventCollector( val client: NostrClient, - val onEvent: (event: Event, relay: Relay) -> Unit, + val onEvent: (event: Event, relay: IRelayClient) -> Unit, ) { private val clientListener = - object : NostrClient.Listener { + object : IRelayClientListener { override fun onEvent( + relay: IRelayClient, + subId: String, event: Event, - subscriptionId: String, - relay: Relay, arrivalTime: Long, afterEOSE: Boolean, ) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt new file mode 100644 index 0000000000..cd2645eed0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.acessories + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId + +fun NostrClient.downloadFirstEvent( + subscriptionId: String = newSubId(), + filters: List = listOf(), + onResponse: (Event) -> Unit, +) { + subscribe( + object : IRelayClientListener { + override fun onEvent( + relay: IRelayClient, + subId: String, + event: Event, + arrivalTime: Long, + afterEOSE: Boolean, + ) { + if (subId == subscriptionId) { + unsubscribe(this) + close(subscriptionId) + + onResponse(event) + } + } + + override fun onClosed( + relay: IRelayClient, + subId: String, + message: String, + ) { + unsubscribe(this) + close(subscriptionId) + super.onClosed(relay, subId, message) + } + + override fun onEOSE( + relay: IRelayClient, + subId: String, + arrivalTime: Long, + ) { + unsubscribe(this) + close(subscriptionId) + super.onEOSE(relay, subId, arrivalTime) + } + + override fun onError( + relay: IRelayClient, + subId: String, + error: Error, + ) { + unsubscribe(this) + close(subscriptionId) + super.onError(relay, subId, error) + } + + override fun onRelayStateChange( + relay: IRelayClient, + type: RelayState, + ) { + if (type == RelayState.DISCONNECTED) { + unsubscribe(this) + close(subscriptionId) + } + super.onRelayStateChange(relay, type) + } + }, + ) + + sendFilter(subscriptionId, filters) +} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayAuthenticator.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayAuthenticator.kt similarity index 79% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayAuthenticator.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayAuthenticator.kt index 93b86c0d89..9e60606e97 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayAuthenticator.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayAuthenticator.kt @@ -18,20 +18,21 @@ * 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.ammolite.relays.datasources +package com.vitorpamplona.quartz.nip01Core.relay.client.acessories import android.util.Log -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.Relay +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient class RelayAuthenticator( val client: NostrClient, - val authenticate: (challenge: String, relay: Relay) -> Unit, + val authenticate: (challenge: String, relay: IRelayClient) -> Unit, ) { private val clientListener = - object : NostrClient.Listener { + object : IRelayClientListener { override fun onAuth( - relay: Relay, + relay: IRelayClient, challenge: String, ) { authenticate(challenge, relay) diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayInsertConfirmationCollector.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayInsertConfirmationCollector.kt similarity index 81% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayInsertConfirmationCollector.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayInsertConfirmationCollector.kt index d4b7b3cf6b..6757dfa763 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayInsertConfirmationCollector.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayInsertConfirmationCollector.kt @@ -18,27 +18,28 @@ * 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.ammolite.relays.datasources +package com.vitorpamplona.quartz.nip01Core.relay.client.acessories import android.util.Log -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient /** * Listens to NostrClient's onEvent messages for caching purposes. */ class RelayInsertConfirmationCollector( val client: NostrClient, - val onRelayReceived: (eventId: HexKey, relay: Relay) -> Unit, + val onRelayReceived: (eventId: HexKey, relay: IRelayClient) -> Unit, ) { private val clientListener = - object : NostrClient.Listener { + object : IRelayClientListener { override fun onSendResponse( + relay: IRelayClient, eventId: String, success: Boolean, message: String, - relay: Relay, ) { if (success) { onRelayReceived(eventId, relay) diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayLogger.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayLogger.kt similarity index 79% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayLogger.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayLogger.kt index bc6721e48c..d1d51a3b70 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayLogger.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayLogger.kt @@ -18,35 +18,36 @@ * 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.ammolite.relays.datasources +package com.vitorpamplona.quartz.nip01Core.relay.client.acessories import android.util.Log -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient /** * Listens to NostrClient's onNotify messages from the relay */ class RelayLogger( val client: NostrClient, - val notify: (message: String, relay: Relay) -> Unit, + val notify: (message: String, relay: IRelayClient) -> Unit, ) { private val clientListener = - object : NostrClient.Listener { + object : IRelayClientListener { /** A new message was received */ override fun onEvent( + relay: IRelayClient, + subId: String, event: Event, - subscriptionId: String, - relay: Relay, arrivalTime: Long, afterEOSE: Boolean, ) { - Log.d("Relay", "Relay onEVENT ${relay.url} ($subscriptionId - $afterEOSE) ${event.toJson()}") + Log.d("Relay", "Relay onEVENT ${relay.url} ($subId - $afterEOSE) ${event.toJson()}") } override fun onSend( - relay: Relay, + relay: IRelayClient, msg: String, success: Boolean, ) { diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayNotifier.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayNotifier.kt similarity index 80% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayNotifier.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayNotifier.kt index 178a6b6763..3d3a33fb65 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/RelayNotifier.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayNotifier.kt @@ -18,27 +18,28 @@ * 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.ammolite.relays.datasources +package com.vitorpamplona.quartz.nip01Core.relay.client.acessories import android.util.Log -import com.vitorpamplona.ammolite.relays.NostrClient -import com.vitorpamplona.ammolite.relays.Relay +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient /** * Listens to NostrClient's onNotify messages from the relay */ class RelayNotifier( val client: NostrClient, - val notify: (message: String, relay: Relay) -> Unit, + val notify: (message: String, relay: IRelayClient) -> Unit, ) { companion object { val TAG = RelayNotifier::class.java.simpleName } private val clientListener = - object : NostrClient.Listener { + object : IRelayClientListener { override fun onNotify( - relay: Relay, + relay: IRelayClient, message: String, ) { notify(message, relay) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/IRelayClientListener.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/IRelayClientListener.kt new file mode 100644 index 0000000000..364776fe5e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/IRelayClientListener.kt @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.listeners + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient + +enum class RelayState { + // Websocket connected + CONNECTED, + + // Websocket disconnecting + DISCONNECTING, + + // Websocket disconnected + DISCONNECTED, +} + +interface IRelayClientListener { + /** + * New Event arrives from the relay. + */ + fun onEvent( + relay: IRelayClient, + subId: String, + event: Event, + arrivalTime: Long, + afterEOSE: Boolean, + ) {} + + /** + * New EOSE command arrives for a subscription + */ + fun onEOSE( + relay: IRelayClient, + subId: String, + arrivalTime: Long, + ) {} + + /** + * New error + */ + fun onError( + relay: IRelayClient, + subId: String, + error: Error, + ) {} + + /** + * Relay is requesting authentication with the given challenge. + */ + fun onAuth( + relay: IRelayClient, + challenge: String, + ) {} + + /** + * called after the relay receives the OK from an Auth message + */ + fun onAuthed( + relay: IRelayClient, + eventId: String, + success: Boolean, + message: String, + ) {} + + /** + * RelayState changes + */ + fun onRelayStateChange( + relay: IRelayClient, + type: RelayState, + ) {} + + /** + * NOTIFY command has arrived. + */ + fun onNotify( + relay: IRelayClient, + description: String, + ) {} + + /** + * Relay closed the subscription + */ + fun onClosed( + relay: IRelayClient, + subId: String, + message: String, + ) {} + + /** + * Triggers this before sending the event. + */ + fun onBeforeSend( + relay: IRelayClient, + event: Event, + ) {} + + /** + * Triggers after the event has been sent. + */ + fun onSend( + relay: IRelayClient, + msg: String, + success: Boolean, + ) {} + + /** + * Relay accepted or rejected the event + */ + fun onSendResponse( + relay: IRelayClient, + eventId: String, + success: Boolean, + message: String, + ) {} +} + +object EmptyClientListener : IRelayClientListener diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RedirectRelayClientListener.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RedirectRelayClientListener.kt new file mode 100644 index 0000000000..75c14b13c1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/listeners/RedirectRelayClientListener.kt @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.listeners + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient + +open class RedirectRelayClientListener( + val listener: IRelayClientListener, +) : IRelayClientListener { + override fun onEvent( + relay: IRelayClient, + subId: String, + event: Event, + arrivalTime: Long, + afterEOSE: Boolean, + ) = listener.onEvent(relay, subId, event, arrivalTime, afterEOSE) + + override fun onEOSE( + relay: IRelayClient, + subId: String, + arrivalTime: Long, + ) = listener.onEOSE(relay, subId, arrivalTime) + + override fun onError( + relay: IRelayClient, + subId: String, + error: Error, + ) = listener.onError(relay, subId, error) + + override fun onAuth( + relay: IRelayClient, + challenge: String, + ) = listener.onAuth(relay, challenge) + + override fun onAuthed( + relay: IRelayClient, + eventId: String, + success: Boolean, + message: String, + ) = listener.onAuthed(relay, eventId, success, message) + + override fun onRelayStateChange( + relay: IRelayClient, + type: RelayState, + ) = listener.onRelayStateChange(relay, type) + + override fun onNotify( + relay: IRelayClient, + description: String, + ) = listener.onNotify(relay, description) + + override fun onClosed( + relay: IRelayClient, + subId: String, + message: String, + ) = listener.onClosed(relay, subId, message) + + override fun onBeforeSend( + relay: IRelayClient, + event: Event, + ) = listener.onBeforeSend(relay, event) + + override fun onSend( + relay: IRelayClient, + msg: String, + success: Boolean, + ) = listener.onSend(relay, msg, success) + + override fun onSendResponse( + relay: IRelayClient, + eventId: String, + success: Boolean, + message: String, + ) = listener.onSendResponse(relay, eventId, success, message) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt new file mode 100644 index 0000000000..1cda88e1d5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.pool + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.any + +class PoolEventOutbox( + val event: Event, + var relays: Set, +) { + private val tries = mutableMapOf() + + fun updateRelays(newRelays: Set) { + relays = newRelays + } + + fun isDone(url: NormalizedRelayUrl) = tries[url]?.let { it.isDone() } ?: false + + fun isDone() = relays.all { isDone(it) } + + fun relaysLeft(): Set = relays.filterTo(mutableSetOf()) { !isDone(it) } + + fun isSupposedToGo(url: NormalizedRelayUrl) = url in relays && !isDone(url) + + fun forEachUnsentEvent( + url: NormalizedRelayUrl, + run: (url: Event) -> Unit, + ) = if (isSupposedToGo(url)) run(event) else null + + fun newTry(url: NormalizedRelayUrl) { + val currentTries = tries[url] + if (currentTries != null) { + currentTries.tries.add(TimeUtils.now()) + } else { + tries.put(url, Tries(mutableListOf(TimeUtils.now()))) + } + } + + fun newResponse( + url: NormalizedRelayUrl, + success: Boolean, + message: String, + ) { + val currentTries = tries[url] + if (currentTries != null) { + currentTries.responses.add(Response(success, message)) + } else { + tries.put( + url, + Tries( + mutableListOf(TimeUtils.now() - 1), + mutableListOf(Response(success, message)), + ), + ) + } + } + + // Tries 3 times + class Tries( + val tries: MutableList = mutableListOf(), + val responses: MutableList = mutableListOf(), + ) { + fun isDone() = responses.any { it.success == true } || responses.size > 2 || tries.size > 3 + } + + class Response( + val success: Boolean, + val message: String, + ) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxRepository.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxRepository.kt new file mode 100644 index 0000000000..b85419258c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxRepository.kt @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.pool + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.MutableStateFlow + +class PoolEventOutboxRepository { + private var eventOutbox = mapOf() + val relays = MutableStateFlow(setOf()) + + fun updateRelays() { + val myRelays = mutableSetOf() + eventOutbox.values.forEach { + myRelays.addAll(it.relaysLeft()) + } + + if (relays.value != myRelays) { + relays.tryEmit(myRelays) + } + } + + fun markAsSending( + event: Event, + relays: Set, + ) { + val currentOutbox = eventOutbox[event.id] + if (currentOutbox == null) { + eventOutbox = eventOutbox + Pair(event.id, PoolEventOutbox(event, relays)) + } else { + currentOutbox.updateRelays(relays) + } + updateRelays() + } + + fun newTry( + id: HexKey, + url: NormalizedRelayUrl, + ) { + val waiting = eventOutbox[id] + if (waiting != null) { + waiting.newTry(url) + } + } + + fun newResponse( + id: HexKey, + url: NormalizedRelayUrl, + success: Boolean, + message: String, + ) { + val waiting = eventOutbox[id] + if (waiting != null) { + waiting.newResponse(url, success, message) + clear() + } + } + + fun clear() { + eventOutbox = eventOutbox.filter { !it.value.isDone() } + updateRelays() + } + + fun forEachUnsentEvent( + url: NormalizedRelayUrl, + run: (url: Event) -> Unit, + ) { + eventOutbox.forEach { + it.value.forEachUnsentEvent(url, run) + } + } +} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/SubscriptionCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscription.kt similarity index 77% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/SubscriptionCache.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscription.kt index 00f3f1e152..1f48aa7ccf 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/SubscriptionCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscription.kt @@ -18,14 +18,12 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.ammolite.relays +package com.vitorpamplona.quartz.nip01Core.relay.client.pool -interface SubscriptionCache { - fun isActive(subscriptionId: String): Boolean +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl - fun allSubscriptions(): Map> - - fun getSubscriptionFilters(subId: String): List - - fun getSubscriptionFiltersOrNull(subId: String): List? +class PoolSubscription( + var filters: List = emptyList(), +) { + fun toFilter(relay: NormalizedRelayUrl) = filters.mapNotNull { it.toFilter(relay) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscriptionRepository.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscriptionRepository.kt new file mode 100644 index 0000000000..19a0aa4044 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscriptionRepository.kt @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.pool + +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.flow.MutableStateFlow + +class PoolSubscriptionRepository { + private var subscriptions = mapOf() + val relays = MutableStateFlow(setOf()) + + fun updateRelays() { + val myRelays = mutableSetOf() + subscriptions.values.forEach { + it.filters.forEach { + if (!myRelays.contains(it.relay)) { + myRelays.add(it.relay) + } + } + } + + if (relays.value != myRelays) { + relays.tryEmit(myRelays) + } + } + + fun addOrUpdate( + subscriptionId: String, + filters: List = listOf(), + ) { + val currentFilter = subscriptions[subscriptionId] + if (currentFilter == null) { + subscriptions = subscriptions + Pair(subscriptionId, PoolSubscription(filters)) + } else { + currentFilter.filters = filters + } + updateRelays() + } + + fun remove(subscriptionId: String) { + if (subscriptions.contains(subscriptionId)) { + subscriptions = subscriptions.minus(subscriptionId) + updateRelays() + } + } + + fun forEachSub( + relay: NormalizedRelayUrl, + run: (String, List) -> Unit, + ) { + subscriptions.forEach { (subId, filters) -> + val filters = filters.toFilter(relay) + if (filters.isNotEmpty()) { + run(subId, filters) + } else { + null + } + } + } + + fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId) + + fun allSubscriptions(): Map = subscriptions + + fun getSubscriptionFilters(subId: String): List = subscriptions[subId]?.filters ?: emptyList() + + fun getSubscriptionFiltersOrNull(subId: String): List? = subscriptions[subId]?.filters +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt new file mode 100644 index 0000000000..d4922d889b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.pool + +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +/** + * Represents a filter that should only be sent to a given relay. + */ +class RelayBasedFilter( + val relay: NormalizedRelayUrl, + val filter: Filter, +) { + // This only exists because some relays confuse empty lists with null lists + fun isValidFor(relay: NormalizedRelayUrl) = relay == this.relay + + fun toFilter(relay: NormalizedRelayUrl): Filter? { + return if (isValidFor(relay)) { + filter + } else { + null + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt new file mode 100644 index 0000000000..484fe760b8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.pool + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.LargeCache +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlin.collections.forEach +import kotlin.collections.isNotEmpty +import kotlin.collections.mapNotNull + +val UnsupportedRelayCreation: (url: NormalizedRelayUrl) -> IRelayClient = { + throw UnsupportedOperationException("Cannot create new relays") +} + +/** + * RelayPool manages the connection to multiple Relays and lets consumers deal with simple events. + */ +class RelayPool( + val listener: IRelayClientListener = EmptyClientListener, + val createNewRelay: (url: NormalizedRelayUrl) -> IRelayClient = UnsupportedRelayCreation, +) : IRelayClientListener { + private val relays = LargeCache() + + // Backing property to avoid flow emissions from other classes + private val _statusFlow = MutableStateFlow(RelayPoolStatus()) + val statusFlow: StateFlow = _statusFlow.asStateFlow() + + fun getRelay(url: NormalizedRelayUrl): IRelayClient? = relays.get(url) + + fun getAll() = relays.keys() + + fun getAllNeedsToReconnect() = relays.filter { url, relay -> relay.needsToReconnect() } + + fun reconnectsRelaysThatNeedTo() { + relays.forEach { url, relay -> + if (relay.needsToReconnect()) { + relay.disconnect() + relay.connect() + } + } + } + + fun connect() = + relays.forEach { url, relay -> + relay.connect() + } + + fun connectIfDisconnected() = + relays.forEach { url, relay -> + relay.connectAndSyncFiltersIfDisconnected() + } + + fun disconnect() = + relays.forEach { url, relay -> + relay.disconnect() + } + + fun sendRequest( + subId: String, + filters: List, + ) { + relays.forEach { url, relay -> + val filters = filters.mapNotNull { it.toFilter(url) } + if (filters.isNotEmpty()) { + relay.sendRequest(subId, filters) + } + } + } + + fun sendCounter( + subId: String, + filters: List, + ) { + relays.forEach { url, relay -> + val filters = filters.mapNotNull { it.toFilter(url) } + if (filters.isNotEmpty()) { + relay.sendRequest(subId, filters) + } + } + } + + fun close(subscriptionId: String) = + relays.forEach { url, relay -> + relay.close(subscriptionId) + } + + fun send( + signedEvent: Event, + list: Set, + ) { + list.forEach { + getOrCreateRelay(it).send(signedEvent) + } + } + + // -------------------- + // Pool Maintenance + // -------------------- + fun getOrCreateRelay(relay: NormalizedRelayUrl) = relays.getOrCreate(relay, createNewRelay) + + fun createRelayIfAbsent(relay: NormalizedRelayUrl): Boolean = relays.createIfAbsent(relay, createNewRelay) + + /** + * Updates the pool of relays without disconnecting the existing ones. + */ + fun updatePool(newRelays: Set) { + val toRemove = relays.keys() - newRelays + var atLeastOne = false + + newRelays.forEach { + if (createRelayIfAbsent(it)) { + atLeastOne = true + } + } + + toRemove.forEach { + if (removeRelayInner(it)) { + atLeastOne = true + } + } + + if (atLeastOne) { + updateStatus() + } + } + + fun addRelay(relay: NormalizedRelayUrl): IRelayClient { + if (createRelayIfAbsent(relay)) { + updateStatus() + } + return getOrCreateRelay(relay) + } + + fun addAllRelays(relayList: List) { + var atLeastOne = false + relayList.forEach { + if (createRelayIfAbsent(it)) { + atLeastOne = true + } + } + if (atLeastOne) { + updateStatus() + } + } + + private fun removeRelayInner(relay: NormalizedRelayUrl): Boolean { + val relayInPool = relays.remove(relay) + if (relayInPool != null) { + relayInPool.disconnect() + return true + } + return false + } + + fun removeRelay(relay: NormalizedRelayUrl) { + if (removeRelayInner(relay)) { + updateStatus() + } + } + + fun removeAllRelays() { + if (relays.size() > 0) { + disconnect() + relays.clear() + updateStatus() + } + } + + // -------------------- + // Listener Interceptor + // -------------------- + + override fun onEvent( + relay: IRelayClient, + subId: String, + event: Event, + arrivalTime: Long, + afterEOSE: Boolean, + ) { + listener.onEvent(relay, subId, event, arrivalTime, afterEOSE) + } + + override fun onError( + relay: IRelayClient, + subId: String, + error: Error, + ) { + listener.onError(relay, subId, error) + updateStatus() + } + + override fun onEOSE( + relay: IRelayClient, + subId: String, + arrivalTime: Long, + ) { + listener.onEOSE(relay, subId, arrivalTime) + updateStatus() + } + + override fun onRelayStateChange( + relay: IRelayClient, + type: RelayState, + ) { + listener.onRelayStateChange(relay, type) + updateStatus() + } + + override fun onSendResponse( + relay: IRelayClient, + eventId: String, + success: Boolean, + message: String, + ) = listener.onSendResponse(relay, eventId, success, message) + + override fun onAuth( + relay: IRelayClient, + challenge: String, + ) = listener.onAuth(relay, challenge) + + override fun onNotify( + relay: IRelayClient, + description: String, + ) = listener.onNotify(relay, description) + + override fun onClosed( + relay: IRelayClient, + subId: String, + message: String, + ) = listener.onClosed(relay, subId, message) + + override fun onSend( + relay: IRelayClient, + msg: String, + success: Boolean, + ) = listener.onSend(relay, msg, success) + + override fun onBeforeSend( + relay: IRelayClient, + event: Event, + ) = listener.onBeforeSend(relay, event) + + // --------------- + // STATUS Reports + // --------------- + + fun availableRelays(): Set = relays.keys() + + fun connectedRelays(): Set = + relays.mapNotNullIntoSet { url, relay -> + if (relay.isConnected()) { + url + } else { + null + } + } + + private fun updateStatus() { + val connected = connectedRelays() + val available = availableRelays() + if (_statusFlow.value.connected != connected || _statusFlow.value.available != available) { + _statusFlow.tryEmit(RelayPoolStatus(connected, available)) + } + } + + @Immutable + data class RelayPoolStatus( + val connected: Set = emptySet(), + val available: Set = emptySet(), + val isConnected: Boolean = connected.isNotEmpty(), + ) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/IRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/IRelayClient.kt new file mode 100644 index 0000000000..76b5627225 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/IRelayClient.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.single + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent + +interface IRelayClient { + val url: NormalizedRelayUrl + + fun connect() + + fun needsToReconnect(): Boolean + + fun connectAndRunAfterSync(onConnected: () -> Unit) + + fun connectAndSyncFiltersIfDisconnected() + + fun isConnected(): Boolean + + fun sendRequest( + subId: String, + filters: List, + ) + + fun sendCount( + subId: String, + filters: List, + ) + + fun send(event: Event) + + fun sendAuth(signedEvent: RelayAuthEvent) + + fun sendEvent(event: Event) + + fun close(subscriptionId: String) + + fun disconnect() +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/Subscription.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/Subscription.kt similarity index 86% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/Subscription.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/Subscription.kt index 29e428b4a0..c2bcf2006d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/Subscription.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/Subscription.kt @@ -18,12 +18,14 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relay +package com.vitorpamplona.quartz.nip01Core.relay.client.single import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import java.util.UUID +import com.vitorpamplona.quartz.utils.RandomInstance + +fun newSubId() = RandomInstance.randomChars(6) class Subscription( - val id: String = UUID.randomUUID().toString().substring(0, 4), + val id: String = newSubId(), val filters: List, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SimpleClientRelay.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt similarity index 54% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SimpleClientRelay.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index ba3c98f880..1aed379245 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SimpleClientRelay.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -18,11 +18,15 @@ * 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.nip01Core.relay +package com.vitorpamplona.quartz.nip01Core.relay.client.single.basic import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage @@ -37,6 +41,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder @@ -46,13 +51,13 @@ import com.vitorpamplona.quartz.utils.bytesUsedInMemory import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.cancellation.CancellationException -class SimpleClientRelay( - val url: String, +open class BasicRelayClient( + override val url: NormalizedRelayUrl, val socketBuilder: WebsocketBuilder, - val subs: SubscriptionCollection, - val listener: Listener, + val listener: IRelayClientListener, val stats: RelayStat = RelayStat(), -) { + val defaultOnConnect: (BasicRelayClient) -> Unit = { }, +) : IRelayClient { companion object { // waits 3 minutes to reconnect once things fail const val DELAY_TO_RECONNECT_IN_MSECS = 500L @@ -70,38 +75,24 @@ class SimpleClientRelay( private val authResponseWatcher = mutableMapOf() private val authChallengesSent = mutableSetOf() - /** - * Auth procedures require us to keep track of the outgoing events - * to make sure the relay waits for the auth to finish and send them. - */ - private val outboxCache = mutableMapOf() - private var connectingMutex = AtomicBoolean() private val parser = ToClientParser() fun isConnectionStarted(): Boolean = socket != null - fun isConnected(): Boolean = socket != null && isReady + override fun isConnected(): Boolean = socket != null && isReady - fun connect() = connectAndRunOverride(::sendEverything) + override fun needsToReconnect() = socket?.needsReconnect() ?: true - fun sendEverything() { - renewSubscriptions() - sendOutbox() - } - - fun sendOutbox() { - synchronized(outboxCache) { - outboxCache.values.forEach { - send(it) - } - } - } - - fun connectAndRunAfterSync(onConnected: () -> Unit) { + override fun connect() = connectAndRunOverride { - sendEverything() + defaultOnConnect(this) + } + + override fun connectAndRunAfterSync(onConnected: () -> Unit) { + connectAndRunOverride { + defaultOnConnect(this) onConnected() } } @@ -122,11 +113,12 @@ class SimpleClientRelay( lastConnectTentative = TimeUtils.now() - socket = socketBuilder.build(url, RelayListener(onConnected)) + socket = socketBuilder.build(url, MyWebsocketListener(onConnected)) socket?.connect() } catch (e: Exception) { if (e is CancellationException) throw e + Log.w("Relay", "Relay Crash before connedting $url", e) stats.newError(e.message ?: "Error trying to connect: ${e.javaClass.simpleName}") markConnectionAsClosed() @@ -136,7 +128,7 @@ class SimpleClientRelay( } } - inner class RelayListener( + inner class MyWebsocketListener( val onConnected: () -> Unit, ) : WebSocketListener { override fun onOpen( @@ -147,22 +139,30 @@ class SimpleClientRelay( markConnectionAsReady(pingMillis, compression) - // Log.w("Relay", "Relay OnOpen, Loading All subscriptions $url") onConnected() - listener.onRelayStateChange(this@SimpleClientRelay, RelayState.CONNECTED) + listener.onRelayStateChange(this@BasicRelayClient, RelayState.CONNECTED) } override fun onMessage(text: String) { stats.addBytesReceived(text.bytesUsedInMemory()) try { - processNewRelayMessage(text) + when (val msg = parser.parse(text)) { + is EventMessage -> processEvent(msg) + is EoseMessage -> processEose(msg) + is NoticeMessage -> processNotice(msg) + is OkMessage -> processOk(msg, onConnected) + is AuthMessage -> processAuth(msg) + is NotifyMessage -> processNotify(msg) + is ClosedMessage -> processClosed(msg) + else -> processUnkownMessage(text) + } } catch (e: Throwable) { if (e is CancellationException) throw e stats.newError("Error processing: $text") - Log.e("Relay", "Error processing: $text") - listener.onError(this@SimpleClientRelay, "", Error("Error processing $text")) + Log.e("Relay", "Error processing: $text from ${url.url}") + listener.onError(this@BasicRelayClient, "", Error("Error processing $text")) } } @@ -172,7 +172,7 @@ class SimpleClientRelay( ) { Log.w("Relay", "Relay onClosing $url: $reason") - listener.onRelayStateChange(this@SimpleClientRelay, RelayState.DISCONNECTING) + listener.onRelayStateChange(this@BasicRelayClient, RelayState.DISCONNECTING) } override fun onClosed( @@ -183,14 +183,15 @@ class SimpleClientRelay( Log.w("Relay", "Relay onClosed $url: $reason") - listener.onRelayStateChange(this@SimpleClientRelay, RelayState.DISCONNECTED) + listener.onRelayStateChange(this@BasicRelayClient, RelayState.DISCONNECTED) } override fun onFailure( t: Throwable, + code: Int?, response: String?, ) { - socket?.cancel() // 1000, "Normal close" + socket?.disconnect() // 1000, "Normal close" // checks if this is an actual failure. Closing the socket generates an onFailure as well. if (!(socket == null && (t.message == "Socket is closed" || t.message == "Socket closed"))) { @@ -200,12 +201,12 @@ class SimpleClientRelay( // Failures disconnect the relay. markConnectionAsClosed() - Log.w("Relay", "Relay onFailure $url, $response $response ${t.message} $socket") + Log.w("Relay", "Relay onFailure $url, $code $response ${t.message} $socket") t.printStackTrace() listener.onError( - this@SimpleClientRelay, + this@BasicRelayClient, "", - Error("WebSocket Failure. Response: $response. Exception: ${t.message}", t), + Error("WebSocket Failure. Response: $code $response. Exception: ${t.message}", t), ) } } @@ -231,73 +232,78 @@ class SimpleClientRelay( this.resetEOSEStatuses() } - fun processNewRelayMessage(newMessage: String) { - when (val msg = parser.parse(newMessage)) { - is EventMessage -> { - listener.onEvent(this, msg.subId, msg.event, TimeUtils.now(), afterEOSEPerSubscription[msg.subId] == true) - } - is EoseMessage -> { - // Log.w("Relay", "Relay onEOSE $url $newMessage") - afterEOSEPerSubscription[msg.subId] = true - listener.onEOSE(this, msg.subId, TimeUtils.now()) - } - is NoticeMessage -> { - // Log.w("Relay", "Relay onNotice $url, $newMessage") - stats.newNotice(msg.message) - listener.onError(this@SimpleClientRelay, msg.message, Error("Relay sent notice: $msg.message")) - } - is OkMessage -> { - Log.w("Relay", "Relay on OK $url, $newMessage") - - // if this is the OK of an auth event, renew all subscriptions and resend all outgoing events. - if (authResponseWatcher.containsKey(msg.eventId)) { - val wasAlreadyAuthenticated = authResponseWatcher[msg.eventId] - authResponseWatcher.put(msg.eventId, msg.success) - if (wasAlreadyAuthenticated != true && msg.success) { - sendEverything() - } - } - - // remove from cache for any error that is not an auth required error. - // for auth required, we will do the auth and try to send again. - if (outboxCache.contains(msg.eventId) && !msg.message.startsWith("auth-required")) { - synchronized(outboxCache) { - outboxCache.remove(msg.eventId) - } - } - - if (!msg.success) { - stats.newNotice("Rejected event ${msg.eventId}: ${msg.message}") - } - - listener.onSendResponse(this@SimpleClientRelay, msg.eventId, msg.success, msg.message) - } - is AuthMessage -> { - // Log.d("Relay", "Relay onAuth $url, $newMessage") - listener.onAuth(this@SimpleClientRelay, msg.challenge) - } - is NotifyMessage -> { - // Log.w("Relay", "Relay onNotify $url, $newMessage") - listener.onNotify(this@SimpleClientRelay, msg.message) - } - is ClosedMessage -> { - afterEOSEPerSubscription[msg.subscriptionId] = false - // Log.w("Relay", "Relay Closed Subscription $url, $newMessage") - listener.onClosed(this@SimpleClientRelay, msg.subscriptionId, msg.message) - } - else -> { - stats.newError("Unsupported message: $newMessage") - Log.w("Relay", "Unsupported message: $newMessage") - listener.onError(this, "", Error("Unsupported message: $newMessage")) - } - } + private fun processEvent(msg: EventMessage) { + listener.onEvent( + relay = this, + subId = msg.subId, + event = msg.event, + arrivalTime = TimeUtils.now(), + afterEOSE = afterEOSEPerSubscription[msg.subId] == true, + ) } - fun disconnect() { + private fun processEose(msg: EoseMessage) { + // Log.w("Relay", "Relay onEOSE $url $newMessage") + afterEOSEPerSubscription[msg.subId] = true + listener.onEOSE(this, msg.subId, TimeUtils.now()) + } + + private fun processNotice(msg: NoticeMessage) { + // Log.w("Relay", "Relay onNotice $url, $newMessage") + stats.newNotice(msg.message) + listener.onError(this@BasicRelayClient, msg.message, Error("Relay sent notice: $msg.message")) + } + + private fun processOk( + msg: OkMessage, + onConnected: () -> Unit, + ) { + Log.w("Relay", "Relay on OK $url, ${msg.eventId} ${msg.success} ${msg.message}") + + // if this is the OK of an auth event, renew all subscriptions and resend all outgoing events. + if (authResponseWatcher.containsKey(msg.eventId)) { + val wasAlreadyAuthenticated = authResponseWatcher[msg.eventId] + authResponseWatcher.put(msg.eventId, msg.success) + if (wasAlreadyAuthenticated != true && msg.success) { + onConnected() + listener.onAuthed(this@BasicRelayClient, msg.eventId, msg.success, msg.message) + } + } + + if (!msg.success) { + stats.newNotice("Rejected event ${msg.eventId}: ${msg.message}") + } + + listener.onSendResponse(this@BasicRelayClient, msg.eventId, msg.success, msg.message) + } + + private fun processAuth(msg: AuthMessage) { + // Log.d("Relay", "Relay onAuth $url, $newMessage") + listener.onAuth(this@BasicRelayClient, msg.challenge) + } + + private fun processNotify(msg: NotifyMessage) { + // Log.w("Relay", "Relay onNotify $url, $newMessage") + listener.onNotify(this@BasicRelayClient, msg.message) + } + + private fun processClosed(msg: ClosedMessage) { + afterEOSEPerSubscription[msg.subscriptionId] = false + // Log.w("Relay", "Relay Closed Subscription $url, $newMessage") + listener.onClosed(this@BasicRelayClient, msg.subscriptionId, msg.message) + } + + private fun processUnkownMessage(newMessage: String) { + stats.newError("Unsupported message: $newMessage") + Log.w("Relay", "Unsupported message: $newMessage") + listener.onError(this, "", Error("Unsupported message: $newMessage")) + } + + override fun disconnect() { Log.d("Relay", "Relay.disconnect $url") lastConnectTentative = 0L // this is not an error, so prepare to reconnect as soon as requested. delayToConnect = DELAY_TO_RECONNECT_IN_MSECS - socket?.cancel() + socket?.disconnect() socket = null isReady = false usingCompression = false @@ -311,15 +317,15 @@ class SimpleClientRelay( authChallengesSent.clear() } - fun sendRequest( - requestId: String, + override fun sendRequest( + subId: String, filters: List, ) { if (isConnectionStarted()) { if (isReady) { if (filters.isNotEmpty()) { - afterEOSEPerSubscription[requestId] = false - writeToSocket(ReqCmd.toJson(requestId, filters)) + afterEOSEPerSubscription[subId] = false + writeToSocket(ReqCmd.Companion.toJson(subId, filters)) } } } else { @@ -332,15 +338,15 @@ class SimpleClientRelay( } } - fun sendCount( - requestId: String, + override fun sendCount( + subId: String, filters: List, ) { if (isConnectionStarted()) { if (isReady) { if (filters.isNotEmpty()) { - afterEOSEPerSubscription[requestId] = false - writeToSocket(CountCmd.toJson(requestId, filters)) + afterEOSEPerSubscription[subId] = false + writeToSocket(CountCmd.Companion.toJson(subId, filters)) } } } else { @@ -353,7 +359,7 @@ class SimpleClientRelay( } } - fun connectAndSendFiltersIfDisconnected() { + override fun connectAndSyncFiltersIfDisconnected() { if (socket == null) { // waits 60 seconds to reconnect after disconnected. if (TimeUtils.now() > lastConnectTentative + delayToConnect) { @@ -363,42 +369,31 @@ class SimpleClientRelay( } } - fun renewSubscriptions() { - // Force update all filters after AUTH. - subs.allSubscriptions().forEach { - sendRequest(requestId = it.id, it.filters) - } - } + override fun send(event: Event) { + listener.onBeforeSend(this@BasicRelayClient, event) - fun send(signedEvent: Event) { - listener.onBeforeSend(this@SimpleClientRelay, signedEvent) - - if (signedEvent is RelayAuthEvent) { - sendAuth(signedEvent) + if (event is RelayAuthEvent) { + sendAuth(event) } else { - sendEvent(signedEvent) + sendEvent(event) } } - fun sendAuth(signedEvent: RelayAuthEvent) { + override fun sendAuth(signedEvent: RelayAuthEvent) { val challenge = signedEvent.challenge() // only send replies to new challenges to avoid infinite loop: if (challenge != null && challenge !in authChallengesSent) { authResponseWatcher[signedEvent.id] = false authChallengesSent.add(challenge) - writeToSocket(AuthCmd.toJson(signedEvent)) + writeToSocket(AuthCmd.Companion.toJson(signedEvent)) } } - fun sendEvent(signedEvent: Event) { - synchronized(outboxCache) { - outboxCache.put(signedEvent.id, signedEvent) - } - + override fun sendEvent(event: Event) { if (isConnectionStarted()) { if (isReady) { - writeToSocket(EventCmd.toJson(signedEvent)) + writeToSocket(EventCmd.Companion.toJson(event)) } } else { // automatically sends all filters after connection is successful. @@ -409,111 +404,20 @@ class SimpleClientRelay( private fun writeToSocket(str: String) { if (socket == null) { listener.onError( - this@SimpleClientRelay, + this@BasicRelayClient, "", Error("Failed to send $str. Relay is not connected."), ) } socket?.let { val result = it.send(str) - listener.onSend(this@SimpleClientRelay, str, result) + listener.onSend(this@BasicRelayClient, str, result) stats.addBytesSent(str.bytesUsedInMemory()) } } - fun close(subscriptionId: String) { - writeToSocket(CloseCmd.toJson(subscriptionId)) + override fun close(subscriptionId: String) { + writeToSocket(CloseCmd.Companion.toJson(subscriptionId)) afterEOSEPerSubscription[subscriptionId] = false } - - interface Listener { - /** - * New Event arrives from the relay. - */ - fun onEvent( - relay: SimpleClientRelay, - subscriptionId: String, - event: Event, - arrivalTime: Long, - afterEOSE: Boolean, - ) - - /** - * New EOSE command arrives for a subscription - */ - fun onEOSE( - relay: SimpleClientRelay, - subscriptionId: String, - arrivalTime: Long, - ) - - /** - * New error - */ - fun onError( - relay: SimpleClientRelay, - subscriptionId: String, - error: Error, - ) - - /** - * Relay is requesting authentication with the given challenge. - */ - fun onAuth( - relay: SimpleClientRelay, - challenge: String, - ) - - /** - * RelayState changes - */ - fun onRelayStateChange( - relay: SimpleClientRelay, - type: RelayState, - ) - - /** - * NOTIFY command has arrived. - */ - fun onNotify( - relay: SimpleClientRelay, - description: String, - ) - - /** - * Relay closed the subscription - */ - fun onClosed( - relay: SimpleClientRelay, - subscriptionId: String, - message: String, - ) - - /** - * Triggers this before sending the event. - */ - fun onBeforeSend( - relay: SimpleClientRelay, - event: Event, - ) - - /** - * Triggers after the event has been sent. - */ - fun onSend( - relay: SimpleClientRelay, - msg: String, - success: Boolean, - ) - - /** - * Relay accepted or rejected the event - */ - fun onSendResponse( - relay: SimpleClientRelay, - eventId: String, - success: Boolean, - message: String, - ) - } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt new file mode 100644 index 0000000000..80d1d378ed --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.single.simple + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RedirectRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent +import com.vitorpamplona.quartz.utils.LargeCache + +class OutboxProtector(listener: IRelayClientListener) : RedirectRelayClientListener(listener) { + /** + * Auth procedures require us to keep track of the outgoing events + * to make sure the relay waits for the auth to finish and send them. + */ + private val outboxCache = LargeCache() + + override fun onRelayStateChange( + relay: IRelayClient, + type: RelayState, + ) { + if (type == RelayState.CONNECTED) { + outboxCache.forEach { id, event -> + relay.sendEvent(event) + } + } + + super.onRelayStateChange(relay, type) + } + + override fun onAuthed( + relay: IRelayClient, + eventId: String, + success: Boolean, + message: String, + ) { + super.onAuthed(relay, eventId, success, message) + outboxCache.forEach { id, event -> + relay.sendEvent(event) + } + } + + override fun onBeforeSend( + relay: IRelayClient, + event: Event, + ) { + if (event !is RelayAuthEvent) { + outboxCache.put(event.id, event) + } + super.onBeforeSend(relay, event) + } + + override fun onSendResponse( + relay: IRelayClient, + eventId: String, + success: Boolean, + message: String, + ) { + // remove from cache for any error that is not an auth required error. + // for auth required, we will do the auth and try to send again. + if (outboxCache.containsKey(eventId) && !message.startsWith("auth-required")) { + outboxCache.remove(eventId) + } + + super.onSendResponse(relay, eventId, success, message) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt new file mode 100644 index 0000000000..35d1bea227 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.single.simple + +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder + +/** + * This relay client saves any event that will be sent in an outbox + * waits for auth and sends it again to make sure it is delivered. + */ +class SimpleRelayClient( + url: NormalizedRelayUrl, + socketBuilder: WebsocketBuilder, + listener: IRelayClientListener, + stats: RelayStat = RelayStat(), + defaultOnConnect: (BasicRelayClient) -> Unit = { }, +) : IRelayClient by BasicRelayClient( + url, + socketBuilder, + OutboxProtector(listener), + stats, + defaultOnConnect, + ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayStat.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStat.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayStat.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStat.kt index aa64801c3e..fd266e0ffc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/RelayStat.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStat.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relay +package com.vitorpamplona.quartz.nip01Core.relay.client.stats import androidx.collection.LruCache import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayStats.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt similarity index 74% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayStats.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt index 033f6673ed..b12e95e2c7 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/RelayStats.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt @@ -18,52 +18,56 @@ * 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.ammolite.relays +package com.vitorpamplona.quartz.nip01Core.relay.client.stats -import com.vitorpamplona.quartz.nip01Core.relay.RelayStat +import android.util.LruCache +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl object RelayStats { - private val innerCache = mutableMapOf() + private val innerCache = + object : LruCache(1000) { + override fun create(key: NormalizedRelayUrl?) = RelayStat() + } - fun get(url: String): RelayStat = innerCache.getOrPut(url) { RelayStat() } + fun get(url: NormalizedRelayUrl): RelayStat = innerCache.get(url) fun addBytesReceived( - url: String, + url: NormalizedRelayUrl, bytesUsedInMemory: Long, ) { get(url).addBytesReceived(bytesUsedInMemory) } fun addBytesSent( - url: String, + url: NormalizedRelayUrl, bytesUsedInMemory: Long, ) { get(url).addBytesSent(bytesUsedInMemory) } fun newError( - url: String, + url: NormalizedRelayUrl, error: String?, ) { get(url).newError(error) } fun newNotice( - url: String, + url: NormalizedRelayUrl, notice: String?, ) { get(url).newNotice(notice) } fun setPing( - url: String, + url: NormalizedRelayUrl, pingInMs: Long, ) { get(url).pingInMs = pingInMs } fun newSpam( - url: String, + url: NormalizedRelayUrl, explanation: String, ) { get(url).newSpam(explanation) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt new file mode 100644 index 0000000000..38abf28e72 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.normalizer + +data class NormalizedRelayUrl(val url: String) : Comparable { + override fun compareTo(other: NormalizedRelayUrl) = url.compareTo(other.url) +} + +fun NormalizedRelayUrl.displayUrl() = + url + .removePrefix("wss://") + .removePrefix("ws://") + .removeSuffix("/") + +fun NormalizedRelayUrl.toHttp() = + if (url.startsWith("wss://")) { + "https${url.drop(3)}" + } else if (url.startsWith("ws://")) { + "https${url.drop(2)}" + } else { + "https://$url" + } + +fun NormalizedRelayUrl.isOnion() = url.endsWith(".onion/") + +fun NormalizedRelayUrl.isLocalHost() = url.contains("127.0.0.1") || url.contains("localhost") diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt new file mode 100644 index 0000000000..f42e8b7d85 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.normalizer + +import androidx.collection.LruCache +import org.czeal.rfc3986.URIReference + +val normalizedUrls = LruCache(5000) + +class RelayUrlNormalizer { + companion object { + fun isLocalHost(url: String) = url.contains("127.0.0.1") || url.contains("localhost") + + fun isOnion(url: String) = url.endsWith(".onion") || url.endsWith(".onion/") + + private fun norm(url: String) = NormalizedRelayUrl(URIReference.parse(url).normalize().toString().intern()) + + fun fix(url: String): String { + val trimmed = url.trim() + + // fast + if (trimmed.length > 4 && trimmed[0] == 'w' && trimmed[1] == 's') { + if (trimmed[2] == 's' && trimmed[3] == ':' && trimmed[4] == '/' && trimmed[5] == '/') { + return trimmed + } else if (trimmed[2] == ':' && trimmed[3] == '/' && trimmed[4] == '/') { + return trimmed + } + } + + // fast + if (trimmed.length > 8 && trimmed[0] == 'h' && trimmed[1] == 't' && trimmed[2] == 't' && trimmed[3] == 'p') { + if (trimmed[4] == 's' && trimmed[5] == ':' && trimmed[6] == '/' && trimmed[7] == '/') { + // https:// + return "wss://${trimmed.drop(8)}" + } else if (trimmed[4] == ':' && trimmed[5] == '/' && trimmed[6] == '/') { + // http:// + return "ws://${trimmed.drop(7)}" + } + } + + return if (isOnion(trimmed) || isLocalHost(trimmed)) { + "ws://$trimmed" + } else { + "wss://$trimmed" + } + } + + fun normalize(url: String): NormalizedRelayUrl { + normalizedUrls.get(url)?.let { return it } + + return try { + val normalized = norm(fix(url)) + normalizedUrls.put(url, normalized) + normalized + } catch (e: Exception) { + NormalizedRelayUrl(url) + } + } + + fun normalizeOrNull(url: String): NormalizedRelayUrl? { + normalizedUrls.get(url)?.let { return it } + + return try { + val normalized = norm(fix(url)) + normalizedUrls.put(url, normalized) + normalized + } catch (e: Exception) { + null + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocket.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocket.kt index a11466a417..090617cba9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocket.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocket.kt @@ -21,9 +21,11 @@ package com.vitorpamplona.quartz.nip01Core.relay.sockets interface WebSocket { + fun needsReconnect(): Boolean + fun connect() - fun cancel() + fun disconnect() fun send(msg: String): Boolean } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt index 32f8808b30..acebf8d6b7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebSocketListener.kt @@ -40,6 +40,7 @@ interface WebSocketListener { fun onFailure( t: Throwable, + code: Int?, response: String?, ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilder.kt index ca813cff6f..b2ca9aa9ac 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/sockets/WebsocketBuilder.kt @@ -20,9 +20,11 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.sockets +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + interface WebsocketBuilder { fun build( - url: String, + url: NormalizedRelayUrl, out: WebSocketListener, ): WebSocket } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt index a36474c752..0dfe23c984 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt @@ -31,6 +31,8 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent abstract class NostrSigner( val pubKey: HexKey, ) { + abstract fun isWriteable(): Boolean + fun sign( ev: EventTemplate, onReady: (T) -> Unit, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt index 53c37c1cf2..9a840abffe 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt @@ -32,6 +32,8 @@ class NostrSignerInternal( ) : NostrSigner(keyPair.pubKey.toHexKey()) { val signerSync = NostrSignerSync(keyPair) + override fun isWriteable(): Boolean = keyPair.privKey != null + override fun sign( createdAt: Long, kind: Int, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt index 8ceb598a50..22cdf14e3f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt @@ -24,33 +24,32 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure import com.vitorpamplona.quartz.utils.pointerSizeInBytes -import com.vitorpamplona.quartz.utils.removeTrailingNullsAndEmptyOthers @Immutable data class ATag( val kind: Int, - val pubKeyHex: String, + val pubKeyHex: HexKey, val dTag: String, - val relay: String? = null, + val relay: NormalizedRelayUrl? = null, ) { - constructor(address: Address, relayHint: String? = null) : this(address.kind, address.pubKeyHex, address.dTag, relayHint) + constructor(address: Address, relayHint: NormalizedRelayUrl? = null) : this(address.kind, address.pubKeyHex, address.dTag, relayHint) fun countMemory(): Long = 5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) 8L + // kind pubKeyHex.bytesUsedInMemory() + dTag.bytesUsedInMemory() + - (relay?.bytesUsedInMemory() ?: 0) + (relay?.url?.bytesUsedInMemory() ?: 0) fun toTag() = Address.assemble(kind, pubKeyHex, dTag) - fun toATagArray() = removeTrailingNullsAndEmptyOthers(TAG_NAME, toTag(), relay) - - fun toQTagArray() = removeTrailingNullsAndEmptyOthers("q", toTag(), relay) + fun toATagArray() = assemble(toTag(), relay) companion object { const val TAG_NAME = "a" @@ -109,7 +108,14 @@ data class ATag( fun parse( aTagId: String, relay: String?, - ) = Address.parse(aTagId)?.let { ATag(it.kind, it.pubKeyHex, it.dTag, relay) } + ) = Address.parse(aTagId)?.let { + ATag( + it.kind, + it.pubKeyHex, + it.dTag, + relay?.let { RelayUrlNormalizer.normalizeOrNull(it) }, + ) + } @JvmStatic fun parse(tag: Array): ATag? { @@ -150,21 +156,31 @@ data class ATag( ensure(tag[1].isNotEmpty()) { return null } ensure(tag[1].contains(':')) { return null } ensure(tag[2].isNotEmpty()) { return null } - return AddressHint(tag[1], tag[2]) + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return AddressHint(tag[1], relayHint) } @JvmStatic fun assemble( aTagId: HexKey, - relay: String?, - ) = arrayOfNotNull(TAG_NAME, aTagId, relay) + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url) + + @JvmStatic + fun assemble( + address: Address, + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, address.toValue(), relay?.url) @JvmStatic fun assemble( kind: Int, pubKey: String, dTag: String, - relay: String?, + relay: NormalizedRelayUrl?, ) = assemble(Address.assemble(kind, pubKey, dTag), relay) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt index c3ccd850a4..9ac889a43e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.quartz.nip01Core.tags.addressables import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -30,7 +32,7 @@ data class Address( val kind: Int, val pubKeyHex: HexKey, val dTag: String, -) { +) : Comparable
{ fun toValue() = assemble(kind, pubKeyHex, dTag) fun countMemory(): Long = @@ -39,6 +41,20 @@ data class Address( pubKeyHex.bytesUsedInMemory() + dTag.bytesUsedInMemory() + override fun compareTo(other: Address): Int { + val result = kind.compareTo(other.kind) + if (result == 0) { + val result2 = pubKeyHex.compareTo(other.pubKeyHex) + if (result2 == 0) { + return dTag.compareTo(other.dTag) + } else { + return result2 + } + } else { + return result + } + } + companion object { fun assemble( kind: Int, @@ -53,8 +69,18 @@ data class Address( if (parts.size > 1 && parts[1].length == 64 && Hex.isHex(parts[1])) { Address(parts[0].toInt(), parts[1], parts[2]) } else { - Log.w("AddressableId", "Error parsing. Pubkey is not hex: $addressId") - null + if (addressId.startsWith("naddr1")) { + val addr = Nip19Parser.uriToRoute(addressId)?.entity + if (addr is NAddress) { + addr.address() + } else { + Log.w("AddressableId", "Error parsing. Pubkey is not hex: $addressId") + null + } + } else { + Log.w("AddressableId", "Error parsing. Pubkey is not hex: $addressId") + null + } } } catch (t: Throwable) { Log.e("AddressableId", "Error parsing: $addressId: ${t.message}", t) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/EventExt.kt index 2d529e766c..ef28d4089b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/EventExt.kt @@ -26,9 +26,9 @@ fun Event.mapTaggedAddress(map: (address: String) -> R) = tags.mapTaggedAddr fun Event.firstIsTaggedAddressableNote(addressableNotes: Set) = tags.firstIsTaggedAddressableNote(addressableNotes) -fun Event.isTaggedAddressableNote(idHex: String) = tags.isTaggedAddressableNote(idHex) +fun Event.isTaggedAddressableNote(addressId: String) = tags.isTaggedAddressableNote(addressId) -fun Event.isTaggedAddressableNotes(idHexes: Set) = tags.isTaggedAddressableNotes(idHexes) +fun Event.isTaggedAddressableNotes(addressIds: Set) = tags.isTaggedAddressableNotes(addressIds) fun Event.isTaggedAddressableKind(kind: Int) = tags.isTaggedAddressableKind(kind) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayBuilderExt.kt index 0ef9971c28..3b494e0e93 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/TagArrayBuilderExt.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.tags.addressables import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip18Reposts.quotes.toQTagArray fun TagArrayBuilder.aTag(tag: ATag) = add(tag.toATagArray()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt index 7c1d7b3181..f9901163f1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -34,10 +36,10 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes data class ETag( override val eventId: HexKey, ) : GenericETag { - override var relay: String? = null + override var relay: NormalizedRelayUrl? = null override var author: HexKey? = null - constructor(eventId: HexKey, relayHint: String? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { + constructor(eventId: HexKey, relayHint: NormalizedRelayUrl? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { this.relay = relayHint this.author = authorPubKeyHex } @@ -45,16 +47,12 @@ data class ETag( fun countMemory(): Long = 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) eventId.bytesUsedInMemory() + - (relay?.bytesUsedInMemory() ?: 0) + + (relay?.url?.bytesUsedInMemory() ?: 0) + (author?.bytesUsedInMemory() ?: 0) fun toNEvent(): String = NEvent.create(eventId, author, null, relay) - override fun toTagArray() = toNamedTagArray(TAG_NAME) - - fun toQTagArray() = toNamedTagArray("q") - - fun toNamedTagArray(key: String) = arrayOfNotNull(key, eventId, relay, author) + override fun toTagArray() = assemble(eventId, relay, author) companion object { const val TAG_NAME = "e" @@ -73,7 +71,9 @@ data class ETag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return ETag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + return ETag(tag[1], hint, tag.getOrNull(3)) } @JvmStatic @@ -90,14 +90,18 @@ data class ETag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return EventIdHint(tag[1], tag[2]) + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(hint != null) { return null } + + return EventIdHint(tag[1], hint) } @JvmStatic fun assemble( eventId: HexKey, - relay: String?, + relay: NormalizedRelayUrl?, author: HexKey?, - ) = arrayOfNotNull(TAG_NAME, eventId, relay, author) + ) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, author) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventReference.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventReference.kt index d5ba844f2e..6b86db0fc1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventReference.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/EventReference.kt @@ -21,9 +21,10 @@ package com.vitorpamplona.quartz.nip01Core.tags.events import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class EventReference( val eventId: HexKey, val author: HexKey?, - val relayHint: String?, + val relayHint: NormalizedRelayUrl?, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/GenericETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/GenericETag.kt index 2514b3bcff..4d2fb2426e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/GenericETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/GenericETag.kt @@ -21,10 +21,11 @@ package com.vitorpamplona.quartz.nip01Core.tags.events import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl interface GenericETag { val eventId: HexKey - val relay: String? + val relay: NormalizedRelayUrl? val author: HexKey? fun toTagArray(): Array diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayBuilderExt.kt index 8604076793..d45141c0e8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/TagArrayBuilderExt.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.tags.events import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip18Reposts.quotes.toQTagArray fun TagArrayBuilder.eTag(tag: ETag) = add(tag.toTagArray()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashtagTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashtagTag.kt index c82e0e8969..581e65a05c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashtagTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/hashtags/HashtagTag.kt @@ -30,6 +30,16 @@ class HashtagTag { @JvmStatic fun isTagged(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + fun isTagged( + tag: Array, + hashtag: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == hashtag + + fun isAnyTagged( + tag: Array, + hashtags: Set, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in hashtags + @JvmStatic fun parse(tag: Array): String? { ensure(tag.has(1)) { return null } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt index 6d0204a2e5..4695d5b1ff 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt @@ -27,6 +27,8 @@ import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -37,12 +39,12 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class PTag( override val pubKey: HexKey, - override val relayHint: String? = null, + override val relayHint: NormalizedRelayUrl? = null, ) : PubKeyReferenceTag { fun countMemory(): Long = 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) pubKey.bytesUsedInMemory() + - (relayHint?.bytesUsedInMemory() ?: 0) + (relayHint?.url?.bytesUsedInMemory() ?: 0) fun toNProfile(): String = NProfile.create(pubKey, relayHint?.let { listOf(it) } ?: emptyList()) @@ -63,7 +65,10 @@ data class PTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return PTag(tag[1], tag.getOrNull(2)) + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return PTag(tag[1], hint) } @JvmStatic @@ -83,13 +88,17 @@ data class PTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return PubKeyHint(tag[1], tag[2]) + + val normalized = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(normalized != null) { return null } + + return PubKeyHint(tag[1], normalized) } @JvmStatic fun assemble( pubkey: HexKey, - relayHint: String?, - ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint) + relayHint: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint?.url) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt index 99da4d7742..27750180bf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PubKeyReferenceTag.kt @@ -21,8 +21,9 @@ package com.vitorpamplona.quartz.nip01Core.tags.people import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl interface PubKeyReferenceTag { val pubKey: HexKey - val relayHint: String? + val relayHint: NormalizedRelayUrl? } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayBuilderExt.kt index 023a23980f..d622c41b2b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/TagArrayBuilderExt.kt @@ -23,10 +23,11 @@ package com.vitorpamplona.quartz.nip01Core.tags.people import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl fun TagArrayBuilder.pTag( pubkey: HexKey, - relayHint: String? = null, + relayHint: NormalizedRelayUrl? = null, ) = add(PTag.assemble(pubkey, relayHint)) fun TagArrayBuilder.pTagIds(tag: Set) = addAll(tag.map { PTag.assemble(it, null) }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt index 92420fa079..c91a6550b0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag @@ -74,7 +76,20 @@ class ContactListEvent( fun followsTags() = hashtags() - fun relays(): Map? = RelaySet.parse(content) + fun relays(): Map? { + val regular = RelaySet.parse(content) + + val normalized = mutableMapOf() + + regular?.forEach { + val key = RelayUrlNormalizer.normalizeOrNull(it.key) + if (key != null) { + normalized.put(key, it.value) + } + } + + return normalized + } companion object { const val KIND = 3 @@ -252,7 +267,7 @@ class ContactListEvent( content = earlierVersion.content, tags = earlierVersion.tags.plus( - element = listOfNotNull("a", aTag.toTag(), aTag.relay).toTypedArray(), + element = aTag.toATagArray(), ), signer = signer, createdAt = createdAt, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt index b1dbef7e05..b67f628b7b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt @@ -26,6 +26,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -36,12 +38,12 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes data class ContactTag( val pubKey: HexKey, ) { - var relayUri: String? = null + var relayUri: NormalizedRelayUrl? = null var petname: String? = null constructor( pubKey: HexKey, - relayHint: String?, + relayHint: NormalizedRelayUrl?, petname: String?, ) : this(pubKey) { this.relayUri = relayHint @@ -51,7 +53,7 @@ data class ContactTag( fun countMemory(): Long = 3 * pointerSizeInBytes + pubKey.bytesUsedInMemory() + - (relayUri?.bytesUsedInMemory() ?: 0) + + (relayUri?.url?.bytesUsedInMemory() ?: 0) + (petname?.bytesUsedInMemory() ?: 0) fun toTagArray() = assemble(pubKey, relayUri, petname) @@ -67,7 +69,10 @@ data class ContactTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return ContactTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return ContactTag(tag[1], hint, tag.getOrNull(3)) } @JvmStatic @@ -75,8 +80,11 @@ data class ContactTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + return try { - ContactTag(decodePublicKey(tag[1]).toHexKey(), tag.getOrNull(2), tag.getOrNull(3)) + ContactTag(decodePublicKey(tag[1]).toHexKey(), hint, tag.getOrNull(3)) } catch (e: Exception) { Log.w("ContactTag", "Can't parse contact list p-tag ${tag.joinToString(", ")}", e) null @@ -110,14 +118,18 @@ data class ContactTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return PubKeyHint(tag[1], tag[2]) + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(hint != null) { return null } + + return PubKeyHint(tag[1], hint) } @JvmStatic fun assemble( pubkey: HexKey, - relayUri: String? = null, + relayUri: NormalizedRelayUrl? = null, petname: String? = null, - ) = arrayOfNotNull(TAG_NAME, pubkey, relayUri, petname) + ) = arrayOfNotNull(TAG_NAME, pubkey, relayUri?.url, petname) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index f130f910ab..e44910158c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -58,6 +58,12 @@ open class BaseThreadedEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider { + @Transient + private var citedUsersCache: Set? = null + + @Transient + private var citedNotesCache: Set? = null + override fun eventHints() = tags.mapNotNull(MarkedETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) @@ -107,10 +113,6 @@ open class BaseThreadedEvent( return newStyleReply ?: newStyleRoot ?: oldStylePositional } - @Transient private var citedUsersCache: Set? = null - - @Transient private var citedNotesCache: Set? = null - fun citedUsers(): Set { citedUsersCache?.let { return it diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt index 48cd5458fc..2c04df5d1e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt @@ -24,43 +24,30 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class MarkedETag( override val eventId: HexKey, ) : GenericETag { - override var relay: String? = null - var marker: String? = null + override var relay: NormalizedRelayUrl? = null + var marker: MARKER? = null override var author: HexKey? = null - constructor(eventId: HexKey, relayHint: String? = null, marker: String? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { + constructor(eventId: HexKey, relayHint: NormalizedRelayUrl? = null, marker: MARKER? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { this.relay = relayHint this.marker = marker this.author = authorPubKeyHex } - constructor(eventId: HexKey, relayHint: String? = null, marker: MARKER? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { - this.relay = relayHint - this.marker = marker?.code - this.author = authorPubKeyHex - } - - fun countMemory(): Long = - 4 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.bytesUsedInMemory() ?: 0) + - (marker?.bytesUsedInMemory() ?: 0) + - (author?.bytesUsedInMemory() ?: 0) - fun toNEvent(): String = NEvent.create(eventId, author, null, relay) - override fun toTagArray() = arrayOfNotNull(TAG_NAME, eventId, relay, marker, author) + override fun toTagArray() = assemble(eventId, relay, marker, author) enum class MARKER( val code: String, @@ -69,6 +56,19 @@ data class MarkedETag( REPLY("reply"), MENTION("mention"), FORK("fork"), + ; + + companion object { + fun parse(code: String): MARKER? { + return when (code) { + ROOT.code -> ROOT + REPLY.code -> REPLY + MENTION.code -> MENTION + FORK.code -> FORK + else -> null + } + } + } } companion object { @@ -92,11 +92,9 @@ data class MarkedETag( return MarkedETag( tag[ORDER_EVT_ID], - tag[ORDER_RELAY], - tag[ORDER_MARKER], - tag.getOrNull( - ORDER_PUBKEY, - ), + RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), + MARKER.parse(tag[ORDER_MARKER]), + tag.getOrNull(ORDER_PUBKEY), ) } @@ -112,38 +110,40 @@ data class MarkedETag( if (tag.size >= 2 && tag[0] == TAG_NAME) { if (tag.size <= 3) { // simple case ["e", "id", "relay"] - MarkedETag(tag[1], tag.getOrNull(2), null as String?, null) + MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, null, null) } else if (tag.size == 4) { + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) if (tag[3].isEmpty()) { // empty tags ["e", "id", "relay", ""] - MarkedETag(tag[1], tag[2], null as String?, null) + MarkedETag(tag[1], relayHint, null, null) } else if (tag[3].length == 64) { // updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"] - MarkedETag(tag[1], tag[2], null as String?, tag[3]) + MarkedETag(tag[1], relayHint, null, tag[3]) } else if (tag[3] == MARKER.ROOT.code) { // corrent root ["e", "id", "relay", "root"] - MarkedETag(tag[1], tag[2], tag[3]) + MarkedETag(tag[1], relayHint, MARKER.ROOT) } else if (tag[3] == MARKER.REPLY.code) { // correct reply ["e", "id", "relay", "reply"] - MarkedETag(tag[1], tag[2], tag[3]) + MarkedETag(tag[1], relayHint, MARKER.REPLY) } else { // ignore "mention" and "fork" markers null } } else { + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) // tag.size >= 5 if (tag[3].isEmpty()) { // empty tags ["e", "id", "relay", "", "pubkey"] - MarkedETag(tag[1], tag[2], null as String?, tag[4]) + MarkedETag(tag[1], relayHint, null, tag[4]) } else if (tag[3].length == 64) { // updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"] - MarkedETag(tag[1], tag[2], null as String?, tag[3]) + MarkedETag(tag[1], relayHint, null, tag[3]) } else if (tag[3] == MARKER.ROOT.code) { // corrent root ["e", "id", "relay", "root"] - MarkedETag(tag[1], tag[2], tag[3], tag[4]) + MarkedETag(tag[1], relayHint, MARKER.ROOT, tag[4]) } else if (tag[3] == MARKER.REPLY.code) { // correct reply ["e", "id", "relay", "reply"] - MarkedETag(tag[1], tag[2], tag[3], tag[4]) + MarkedETag(tag[1], relayHint, MARKER.REPLY, tag[4]) } else { // ignore "mention" and "fork" markers null @@ -189,21 +189,24 @@ data class MarkedETag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return EventIdHint(tag[1], tag[2]) + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(hint != null) { return null } + + return EventIdHint(tag[1], hint) } @JvmStatic fun parseRoot(tag: Array): MarkedETag? { if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null if (tag[ORDER_MARKER] != MARKER.ROOT.code) return null + // ["e", id hex, relay hint, marker, pubkey] return MarkedETag( - tag[ORDER_EVT_ID], - tag[ORDER_RELAY], - tag[ORDER_MARKER], - tag.getOrNull( - ORDER_PUBKEY, - ), + eventId = tag[ORDER_EVT_ID], + relayHint = RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), + marker = MARKER.ROOT, + authorPubKeyHex = tag.getOrNull(ORDER_PUBKEY), ) } @@ -213,7 +216,7 @@ data class MarkedETag( @JvmStatic fun parseUnmarkedRoot(tag: Array): MarkedETag? = if (tag.size in 2..3 && tag[0] == TAG_NAME) { - MarkedETag(tag[1], tag.getOrNull(2), MARKER.ROOT) + MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, MARKER.ROOT) } else { null } @@ -225,8 +228,8 @@ data class MarkedETag( // ["e", id hex, relay hint, marker, pubkey] return MarkedETag( tag[ORDER_EVT_ID], - tag[ORDER_RELAY], - tag[ORDER_MARKER], + RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), + MARKER.REPLY, tag.getOrNull( ORDER_PUBKEY, ), @@ -239,7 +242,7 @@ data class MarkedETag( @JvmStatic fun parseUnmarkedReply(tag: Array): MarkedETag? = if (tag.size in 2..3 && tag[0] == TAG_NAME) { - MarkedETag(tag[1], tag.getOrNull(2), MARKER.REPLY) + MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, MARKER.REPLY) } else { null } @@ -255,9 +258,17 @@ data class MarkedETag( @JvmStatic fun assemble( eventId: HexKey, - relay: String?, + relay: NormalizedRelayUrl?, + marker: String?, + author: HexKey?, + ) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, marker, author) + + @JvmStatic + fun assemble( + eventId: HexKey, + relay: NormalizedRelayUrl?, marker: MARKER?, author: HexKey?, - ) = arrayOfNotNull(TAG_NAME, eventId, relay, marker?.code, author) + ) = assemble(eventId, relay, marker?.code, author) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/ReplyBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/ReplyBuilder.kt index 2ab00d3448..3f77f66788 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/ReplyBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/ReplyBuilder.kt @@ -54,7 +54,7 @@ fun prepareETagsAsReplyTo( val branchTags = replyingTo.event.threadTags().filter { it.eventId != rootTag.eventId }.map { - MarkedETag(it.eventId, it.relay, "", it.author) + MarkedETag(it.eventId, it.relay, null, it.author) } val branch = mutableListOf() @@ -83,7 +83,7 @@ fun prepareMarkedETagsAsReplyTo(replyingTo: EventHintBun val branchTags = replyingTo.event.threadTags().filter { it.eventId != rootTag.eventId }.map { - MarkedETag(it.eventId, it.relay, "", it.author) + MarkedETag(it.eventId, it.relay, null, it.author) } val branch = mutableListOf() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt index e4d9c059e7..6293e094c5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/messages/ChatMessageEvent.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip17Dm.base.BaseDMGroupEvent -import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent.Companion.ALT_DESCRIPTION import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @@ -45,7 +44,7 @@ class ChatMessageEvent( companion object { const val KIND = 14 - const val ALT = "Direct message" + const val ALT_DESCRIPTION = "Direct message" fun build( msg: String, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt index c4ba35d46d..ac5a6eb2b5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt @@ -23,10 +23,12 @@ package com.vitorpamplona.quartz.nip17Dm.settings import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip17Dm.settings.tags.RelayTag import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -39,14 +41,7 @@ class ChatMessageRelayListEvent( content: String, sig: HexKey, ) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun relays(): List = - tags.mapNotNull { - if (it.size > 1 && it[0] == "relay") { - it[1] - } else { - null - } - } + fun relays(): List = tags.mapNotNull(RelayTag::parse) companion object { const val KIND = 10050 @@ -57,26 +52,27 @@ class ChatMessageRelayListEvent( fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) - fun createTagArray(relays: List): Array> = + fun createTagArray(relays: List): Array> = relays .map { - arrayOf("relay", it) - }.plusElement(AltTag.assemble("Relay list to receive private messages")) + RelayTag.assemble(it) + }.plusElement( + AltTag.assemble("Relay list to receive private messages"), + ) .toTypedArray() fun updateRelayList( earlierVersion: ChatMessageRelayListEvent, - relays: List, + relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ChatMessageRelayListEvent) -> Unit, ) { val tags = - earlierVersion.tags - .filter { it[0] != "relay" } + earlierVersion.tags.filter(RelayTag::notMatch) .plus( relays.map { - arrayOf("relay", it) + RelayTag.assemble(it) }, ).toTypedArray() @@ -84,7 +80,7 @@ class ChatMessageRelayListEvent( } fun createFromScratch( - relays: List, + relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ChatMessageRelayListEvent) -> Unit, @@ -93,7 +89,7 @@ class ChatMessageRelayListEvent( } fun create( - relays: List, + relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (ChatMessageRelayListEvent) -> Unit, @@ -102,7 +98,7 @@ class ChatMessageRelayListEvent( } fun create( - relays: List, + relays: List, signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), ): ChatMessageRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "") diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/tags/RelayTag.kt new file mode 100644 index 0000000000..19a2762cd9 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/tags/RelayTag.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip17Dm.settings.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.ensure + +class RelayTag { + companion object { + const val TAG_NAME = "relay" + + @JvmStatic + fun match(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun notMatch(tag: Array) = tag.has(0) && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): NormalizedRelayUrl? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null + + return relay + } + + @JvmStatic + fun assemble(relay: NormalizedRelayUrl) = arrayOf(TAG_NAME, relay.url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt index 6d76eac17a..2439e25cf1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag @@ -86,8 +87,8 @@ class GenericRepostEvent( fun build( boostedPost: Event, - eventSourceRelay: String?, - authorHomeRelay: String?, + eventSourceRelay: NormalizedRelayUrl?, + authorHomeRelay: NormalizedRelayUrl?, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, boostedPost.toJson(), createdAt) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt index d6056b5103..05aa903e45 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag @@ -84,8 +85,8 @@ class RepostEvent( fun build( boostedPost: Event, - eventSourceRelay: String?, - authorHomeRelay: String?, + eventSourceRelay: NormalizedRelayUrl?, + authorHomeRelay: NormalizedRelayUrl?, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, boostedPost.toJson(), createdAt) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt index 4737876e4a..f9e3d746a8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt @@ -21,10 +21,13 @@ package com.vitorpamplona.quartz.nip18Reposts.quotes import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.entities.Note +import org.apache.commons.lang3.text.translate.CharSequenceTranslator.hex fun Note.toQuoteTag() = QEventTag(hex, null, null) @@ -51,3 +54,7 @@ fun NEmbed.toQuoteTagArray() = } else { QEventTag.assemble(event.id, null, event.pubKey) } + +fun ETag.toQTagArray() = QEventTag.assemble(eventId, relay, author) + +fun ATag.toQTagArray() = QAddressableTag.assemble(kind, pubKeyHex, dTag, relay) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt index 77008816d0..b25b6ebfa7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip18Reposts.quotes import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -33,11 +35,11 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes data class QAddressableTag( val address: Address, ) : QTag { - var relay: String? = null + var relay: NormalizedRelayUrl? = null constructor( address: Address, - relayHint: String?, + relayHint: NormalizedRelayUrl?, ) : this(address) { this.relay = relayHint } @@ -46,7 +48,7 @@ data class QAddressableTag( kind: Int, pubKeyHex: HexKey, dTag: String, - relayHint: String?, + relayHint: NormalizedRelayUrl?, ) : this(Address(kind, pubKeyHex, dTag)) { this.relay = relayHint } @@ -54,7 +56,7 @@ data class QAddressableTag( fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + - (relay?.bytesUsedInMemory() ?: 0) + (relay?.url?.bytesUsedInMemory() ?: 0) override fun toTagArray() = assemble(address, relay) @@ -67,7 +69,8 @@ data class QAddressableTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length != 64) { return null } val address = Address.parse(tag[1]) ?: return null - return QAddressableTag(address, tag.getOrNull(2)) + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + return QAddressableTag(address, hint) } @JvmStatic @@ -75,13 +78,13 @@ data class QAddressableTag( kind: Int, pubKeyHex: HexKey, dTag: String, - relay: String?, - ) = arrayOfNotNull(TAG_NAME, Address.assemble(kind, pubKeyHex, dTag), relay) + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, Address.assemble(kind, pubKeyHex, dTag), relay?.url) @JvmStatic fun assemble( address: Address, - relay: String?, - ) = arrayOfNotNull(TAG_NAME, address.toValue(), relay) + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, address.toValue(), relay?.url) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt index ed8a22da13..662c6f9b91 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip18Reposts.quotes import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure @@ -32,10 +34,10 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes data class QEventTag( val eventId: HexKey, ) : QTag { - var relay: String? = null + var relay: NormalizedRelayUrl? = null var author: HexKey? = null - constructor(eventId: HexKey, relayHint: String? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { + constructor(eventId: HexKey, relayHint: NormalizedRelayUrl? = null, authorPubKeyHex: HexKey? = null) : this(eventId) { this.relay = relayHint this.author = authorPubKeyHex } @@ -43,7 +45,7 @@ data class QEventTag( fun countMemory(): Long = 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) eventId.bytesUsedInMemory() + - (relay?.bytesUsedInMemory() ?: 0) + + (relay?.url?.bytesUsedInMemory() ?: 0) + (author?.bytesUsedInMemory() ?: 0) override fun toTagArray() = assemble(eventId, relay, author) @@ -56,14 +58,17 @@ data class QEventTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return QEventTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return QEventTag(tag[1], hint, tag.getOrNull(3)) } @JvmStatic fun assemble( eventId: HexKey, - relay: String?, + relay: NormalizedRelayUrl?, author: HexKey?, - ) = arrayOfNotNull(TAG_NAME, eventId, relay, author) + ) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, author) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt index 46bdcf6392..f059e9a90e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip18Reposts.quotes import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.ensure @@ -36,11 +37,14 @@ interface QTag { fun parse(tag: Array): QTag? { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } + + val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + return if (tag[1].length == 64) { - QEventTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + QEventTag(tag[1], relayHint, tag.getOrNull(3)) } else { val address = Address.parse(tag[1]) ?: return null - QAddressableTag(address, tag.getOrNull(2)) + QAddressableTag(address, relayHint) } } @@ -53,21 +57,29 @@ interface QTag { @JvmStatic fun parseEventAsHint(tag: Array): EventIdHint? { - ensure(tag.has(1)) { return null } + ensure(tag.has(2)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return EventIdHint(tag[1], tag[2]) + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return EventIdHint(tag[1], relayHint) } @JvmStatic fun parseAddressAsHint(tag: Array): AddressHint? { - ensure(tag.has(1)) { return null } + ensure(tag.has(2)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length != 64) { return null } ensure(tag[2].isNotEmpty()) { return null } ensure(!tag[1].contains(':')) { return null } - return AddressHint(tag[1], tag[2]) + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return AddressHint(tag[1], relayHint) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ATagExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ATagExt.kt index c46b594995..dc9bb9d641 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ATagExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ATagExt.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.quartz.nip19Bech32 import android.util.Log +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.utils.Hex @@ -44,7 +46,10 @@ fun ATag.Companion.parseAtag( try { val parts = atag.split(":", limit = 3) Hex.decode(parts[1]) - ATag(parts[0].toInt(), parts[1], parts[2], relay) + + val relayHint = relay?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + ATag(parts[0].toInt(), parts[1], parts[2], relayHint) } catch (t: Throwable) { Log.w("ATag", "Error parsing A Tag: $atag: ${t.message}") null @@ -58,7 +63,7 @@ fun ATag.Companion.parseAtagUnckecked(atag: String): ATag? = null } -fun ATag.toNAddr(overrideRelay: String? = relay): String = NAddress.create(kind, pubKeyHex, dTag, overrideRelay ?: relay) +fun ATag.toNAddr(overrideRelay: NormalizedRelayUrl? = relay): String = NAddress.create(kind, pubKeyHex, dTag, overrideRelay ?: relay) fun ATag.Companion.parseNAddr(naddr: String) = NAddress.parse(naddr)?.let { result -> diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/EventExt.kt index 151fa16c43..8d96412d4a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/EventExt.kt @@ -22,10 +22,11 @@ package com.vitorpamplona.quartz.nip19Bech32 import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -fun Event.toNIP19(relayHint: String? = null): String = +fun Event.toNIP19(relayHint: NormalizedRelayUrl? = null): String = if (this is AddressableEvent) { ATag(kind, pubKey, dTag(), relayHint).toNAddr() } else { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/TlvBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/TlvBuilderExt.kt index 8deadc46c3..cf9525037d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/TlvBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/TlvBuilderExt.kt @@ -42,6 +42,11 @@ fun TlvBuilder.addStringIfNotNull( data: String?, ) = addStringIfNotNull(type.id, data) +fun TlvBuilder.addStringIfNotBlank( + type: TlvTypes, + data: String, +) = addStringIfNotBlank(type.id, data) + fun TlvBuilder.addHexIfNotNull( type: TlvTypes, data: HexKey?, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt index 52955108ca..d16627a6c4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt @@ -23,9 +23,11 @@ package com.vitorpamplona.quartz.nip19Bech32.entities import addHex import addInt import addString -import addStringIfNotNull +import addStringIfNotBlank import android.util.Log import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip19Bech32.TlvTypes import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes @@ -38,10 +40,12 @@ data class NAddress( val kind: Int, val author: String, val dTag: String, - val relay: List, + val relay: List, ) : Entity { fun aTag(): String = Address.assemble(kind, author, dTag) + fun address() = Address(kind, author, dTag) + companion object { fun parse(naddr: String): NAddress? { try { @@ -68,20 +72,37 @@ data class NAddress( val author = tlv.firstAsHex(TlvTypes.AUTHOR.id) ?: return null val kind = tlv.firstAsInt(TlvTypes.KIND.id) ?: return null - return NAddress(kind, author, d, relay) + return NAddress(kind, author, d, relay.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }) } fun create( kind: Int, pubKeyHex: String, dTag: String, - vararg relays: String?, + relay: NormalizedRelayUrl?, + ): String = + TlvBuilder() + .apply { + addString(TlvTypes.SPECIAL, dTag) + if (relay != null) { + addStringIfNotBlank(TlvTypes.RELAY, relay.url) + } + addHex(TlvTypes.AUTHOR, pubKeyHex) + addInt(TlvTypes.KIND, kind) + }.build() + .toNAddress() + + fun create( + kind: Int, + pubKeyHex: String, + dTag: String, + relays: List, ): String = TlvBuilder() .apply { addString(TlvTypes.SPECIAL, dTag) relays.forEach { - addStringIfNotNull(TlvTypes.RELAY, it) + addStringIfNotBlank(TlvTypes.RELAY, it.url) } addHex(TlvTypes.AUTHOR, pubKeyHex) addInt(TlvTypes.KIND, kind) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEvent.kt index 62114d1d82..308220c8f4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEvent.kt @@ -25,6 +25,8 @@ import addHexIfNotNull import addIntIfNotNull import addStringIfNotNull import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.TlvTypes import com.vitorpamplona.quartz.nip19Bech32.asStringList import com.vitorpamplona.quartz.nip19Bech32.firstAsHex @@ -35,7 +37,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNEvent @Immutable data class NEvent( val hex: String, - val relay: List, + val relay: List, val author: String?, val kind: Int?, ) : Entity { @@ -52,20 +54,42 @@ data class NEvent( if (hex.isBlank()) return null - return NEvent(hex, relay, author, kind) + return NEvent( + hex, + relay.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }, + author, + kind, + ) } fun create( idHex: String, author: String?, kind: Int?, - vararg relays: String?, + relay: NormalizedRelayUrl?, + ): String = + TlvBuilder() + .apply { + addHex(TlvTypes.SPECIAL, idHex) + if (relay != null) { + addStringIfNotNull(TlvTypes.RELAY, relay.url) + } + addHexIfNotNull(TlvTypes.AUTHOR, author) + addIntIfNotNull(TlvTypes.KIND, kind) + }.build() + .toNEvent() + + fun create( + idHex: String, + author: String?, + kind: Int?, + relays: List, ): String = TlvBuilder() .apply { addHex(TlvTypes.SPECIAL, idHex) relays.forEach { - addStringIfNotNull(TlvTypes.RELAY, it) + addStringIfNotNull(TlvTypes.RELAY, it.url) } addHexIfNotNull(TlvTypes.AUTHOR, author) addIntIfNotNull(TlvTypes.KIND, kind) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NProfile.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NProfile.kt index 4c84539956..55c84758f1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NProfile.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NProfile.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.nip19Bech32.entities import addHex import addStringIfNotNull import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.TlvTypes import com.vitorpamplona.quartz.nip19Bech32.asStringList import com.vitorpamplona.quartz.nip19Bech32.firstAsHex @@ -33,7 +35,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNProfile @Immutable data class NProfile( val hex: String, - val relay: List, + val relay: List, ) : Entity { companion object { fun parse(bytes: ByteArray): NProfile? { @@ -46,18 +48,18 @@ data class NProfile( if (hex.isBlank()) return null - return NProfile(hex, relay) + return NProfile(hex, relay.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }) } fun create( authorPubKeyHex: String, - relays: List, + relays: List, ): String = TlvBuilder() .apply { addHex(TlvTypes.SPECIAL, authorPubKeyHex) relays.forEach { - addStringIfNotNull(TlvTypes.RELAY, it) + addStringIfNotNull(TlvTypes.RELAY, it.url) } }.build() .toNProfile() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NSec.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NSec.kt index c422393108..6f0bf29546 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NSec.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NSec.kt @@ -21,12 +21,16 @@ package com.vitorpamplona.quartz.nip19Bech32.entities import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 @Immutable data class NSec( val hex: String, ) : Entity { + fun toPubKeyHex() = Nip01.pubKeyCreate(hex.hexToByteArray()).toHexKey() + companion object { fun parse(bytes: ByteArray): NSec? { if (bytes.isEmpty()) return null diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt index a515d26768..b09881d57b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/tlv/TlvBuilder.kt @@ -55,6 +55,15 @@ class TlvBuilder { data: String?, ) = data?.let { addString(type, it) } + fun addStringIfNotBlank( + type: Byte, + data: String, + ) { + if (data.isNotBlank()) { + addString(type, data) + } + } + fun addHexIfNotNull( type: Byte, data: HexKey?, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt index 54d7eb2631..28cca61d37 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip21UriScheme/EventExt.kt @@ -22,8 +22,9 @@ package com.vitorpamplona.quartz.nip21UriScheme import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip19Bech32.toNIP19 -fun Event.toNostrUri(relayHint: String? = null): String = "nostr:${toNIP19(relayHint)}" +fun Event.toNostrUri(relayHint: NormalizedRelayUrl? = null): String = "nostr:${toNIP19(relayHint)}" fun EventHintBundle.toNostrUri(): String = "nostr:${toNEvent()}" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt index ddc263e61e..d871c54890 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/TagArrayBuilderExt.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip22Comments import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTags @@ -39,12 +40,12 @@ import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId fun TagArrayBuilder.rootAddress( addressId: String, - relayHint: String?, + relayHint: NormalizedRelayUrl?, ) = addUnique(RootAddressTag.assemble(addressId, relayHint)) fun TagArrayBuilder.rootEvent( eventId: String, - relayHint: String?, + relayHint: NormalizedRelayUrl?, pubkey: String?, ) = addUnique(RootEventTag.assemble(eventId, relayHint, pubkey)) @@ -60,17 +61,17 @@ fun TagArrayBuilder.rootKind(id: ExternalId) = addUnique(RootKindT fun TagArrayBuilder.rootAuthor( pubKey: HexKey, - relay: String?, + relay: NormalizedRelayUrl?, ) = add(RootAuthorTag.assemble(pubKey, relay)) fun TagArrayBuilder.replyAddress( addressId: String, - relayHint: String?, + relayHint: NormalizedRelayUrl?, ) = addUnique(ReplyAddressTag.assemble(addressId, relayHint)) fun TagArrayBuilder.replyEvent( eventId: String, - relayHint: String?, + relayHint: NormalizedRelayUrl?, pubkey: String?, ) = addUnique(ReplyEventTag.assemble(eventId, relayHint, pubkey)) @@ -86,7 +87,7 @@ fun TagArrayBuilder.replyKind(id: ExternalId) = addUnique(ReplyKin fun TagArrayBuilder.replyAuthor( pubKey: HexKey, - relay: String?, + relay: NormalizedRelayUrl?, ) = add(ReplyAuthorTag.assemble(pubKey, relay)) fun TagArrayBuilder.notify(list: List) = pTags(list) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt index 28d4a18275..4582ce1733 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAddressTag.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.ensure @@ -32,7 +34,7 @@ import com.vitorpamplona.quartz.utils.ensure @Immutable class ReplyAddressTag( val addressId: String, - val relay: String? = null, + val relay: NormalizedRelayUrl? = null, ) { fun toTagArray() = assemble(addressId, relay) @@ -47,7 +49,10 @@ class ReplyAddressTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return ReplyAddressTag(tag[1], tag.getOrNull(2)) + + val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return ReplyAddressTag(tag[1], relayHint) } @JvmStatic @@ -64,21 +69,25 @@ class ReplyAddressTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return AddressHint(tag[1], tag[2]) + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return AddressHint(tag[1], relayHint) } @JvmStatic fun assemble( addressId: HexKey, - relay: String?, - ) = arrayOfNotNull(TAG_NAME, addressId, relay) + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, addressId, relay?.url) @JvmStatic fun assemble( kind: Int, pubKey: String, dTag: String, - relay: String?, + relay: NormalizedRelayUrl?, ) = assemble(Address.assemble(kind, pubKey, dTag), relay) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt index 8d7c3749f5..f006a731e1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyAuthorTag.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.ensure @@ -32,7 +34,7 @@ import com.vitorpamplona.quartz.utils.ensure @Immutable data class ReplyAuthorTag( override val pubKey: HexKey, - override val relayHint: String? = null, + override val relayHint: NormalizedRelayUrl? = null, ) : PubKeyReferenceTag { fun toTagArray() = assemble(pubKey, relayHint) @@ -47,7 +49,10 @@ data class ReplyAuthorTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return ReplyAuthorTag(tag[1], tag.getOrNull(2)) + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return ReplyAuthorTag(tag[1], hint) } @JvmStatic @@ -64,13 +69,17 @@ data class ReplyAuthorTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return PubKeyHint(tag[1], tag[2]) + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(hint != null) { return null } + + return PubKeyHint(tag[1], hint) } @JvmStatic fun assemble( pubkey: HexKey, - relayHint: String?, - ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint) + relayHint: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint?.url) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt index 6d4e42de16..4f891256f2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyEventTag.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -34,7 +36,7 @@ import com.vitorpamplona.quartz.utils.ensure class ReplyEventTag( val ref: EventReference, ) { - constructor(eventId: String, relayHint: String?, pubkey: String?) : this(EventReference(eventId, relayHint, pubkey)) + constructor(eventId: String, relayHint: NormalizedRelayUrl?, pubkey: String?) : this(EventReference(eventId, pubkey, relayHint)) fun toTagArray() = assemble(ref) @@ -61,7 +63,7 @@ class ReplyEventTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return ReplyEventTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + return ReplyEventTag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, tag.getOrNull(3)) } @JvmStatic @@ -87,15 +89,19 @@ class ReplyEventTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return EventIdHint(tag[1], tag[2]) + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return EventIdHint(tag[1], relayHint) } @JvmStatic fun assemble( eventId: HexKey, - relay: String?, + relay: NormalizedRelayUrl?, pubkey: String?, - ) = arrayOfNotNull(TAG_NAME, eventId, relay, pubkey) + ) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, pubkey) @JvmStatic fun assemble(ref: EventReference) = assemble(ref.eventId, ref.relayHint, ref.author) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt index 17cd967317..9278c3e0cd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAddressTag.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.ensure @@ -32,7 +34,7 @@ import com.vitorpamplona.quartz.utils.ensure @Immutable class RootAddressTag( val addressId: String, - val relay: String? = null, + val relay: NormalizedRelayUrl? = null, ) { fun toTagArray() = assemble(addressId, relay) @@ -59,7 +61,10 @@ class RootAddressTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - return RootAddressTag(tag[1], tag.getOrNull(2)) + + val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return RootAddressTag(tag[1], relayHint) } @JvmStatic @@ -84,21 +89,25 @@ class RootAddressTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return AddressHint(tag[1], tag[2]) + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return AddressHint(tag[1], relayHint) } @JvmStatic fun assemble( addressId: HexKey, - relay: String?, - ) = arrayOfNotNull(TAG_NAME, addressId, relay) + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, addressId, relay?.url) @JvmStatic fun assemble( kind: Int, pubKey: String, dTag: String, - relay: String?, + relay: NormalizedRelayUrl?, ) = assemble(Address.assemble(kind, pubKey, dTag), relay) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt index 469600c52d..a4afdeda8b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootAuthorTag.kt @@ -26,6 +26,8 @@ import com.vitorpamplona.quartz.nip01Core.core.PUBKEY_LENGTH import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.ensure @@ -33,7 +35,7 @@ import com.vitorpamplona.quartz.utils.ensure @Immutable data class RootAuthorTag( override val pubKey: HexKey, - override val relayHint: String? = null, + override val relayHint: NormalizedRelayUrl? = null, ) : PubKeyReferenceTag { fun toTagArray() = assemble(pubKey, relayHint) @@ -46,7 +48,10 @@ data class RootAuthorTag( @JvmStatic fun parse(tag: Tag): ReplyAuthorTag? { if (tag.size < 2 || tag[0] != TAG_NAME || tag[1].length != PUBKEY_LENGTH) return null - return ReplyAuthorTag(tag[1], tag.getOrNull(2)) + + val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return ReplyAuthorTag(tag[1], relayHint) } @JvmStatic @@ -54,7 +59,10 @@ data class RootAuthorTag( ensure(tag.size >= 2) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == PUBKEY_LENGTH) { return null } - return ReplyAuthorTag(tag[1], tag.getOrNull(2)) + + val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return ReplyAuthorTag(tag[1], relayHint) } @JvmStatic @@ -69,13 +77,17 @@ data class RootAuthorTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return PubKeyHint(tag[1], tag[2]) + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return PubKeyHint(tag[1], relayHint) } @JvmStatic fun assemble( pubkey: HexKey, - relayHint: String?, - ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint) + relayHint: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint?.url) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt index 3ca2355cce..ce210e6e8a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/RootEventTag.kt @@ -25,6 +25,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -34,7 +36,7 @@ import com.vitorpamplona.quartz.utils.ensure class RootEventTag( val ref: EventReference, ) { - constructor(eventId: String, relayHint: String?, pubkey: String?) : this(EventReference(eventId, relayHint, pubkey)) + constructor(eventId: String, relayHint: NormalizedRelayUrl?, pubkey: String?) : this(EventReference(eventId, pubkey, relayHint)) fun toTagArray() = assemble(ref) @@ -61,7 +63,10 @@ class RootEventTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return RootEventTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + + val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return RootEventTag(tag[1], relayHint, tag.getOrNull(3)) } @JvmStatic @@ -87,15 +92,19 @@ class RootEventTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - return EventIdHint(tag[1], tag[2]) + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return EventIdHint(tag[1], relayHint) } @JvmStatic fun assemble( eventId: HexKey, - relay: String?, + relay: NormalizedRelayUrl?, pubkey: String?, - ) = arrayOfNotNull(TAG_NAME, eventId, relay, pubkey) + ) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, pubkey) @JvmStatic fun assemble(ref: EventReference) = assemble(ref.eventId, ref.relayHint, ref.author) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt index 01f2270f8c..e130211f10 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -66,7 +67,7 @@ class LongTextNoteEvent( override fun dTag() = tags.dTag() - override fun aTag(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint) + override fun aTag(relayHint: NormalizedRelayUrl?) = ATag(kind, pubKey, dTag(), relayHint) override fun address() = Address(kind, pubKey, dTag()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index aa6a78b8be..d7925bd075 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -20,14 +20,19 @@ */ package com.vitorpamplona.quartz.nip28PublicChat.admin +import android.R.attr.data +import android.util.Log import androidx.compose.runtime.Immutable +import com.fasterxml.jackson.core.JsonParseException import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData +import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @@ -41,9 +46,16 @@ class ChannelCreateEvent( sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider { - override fun eventHints() = channelInfo().relays?.map { EventIdHint(id, it) } ?: emptyList() + override fun eventHints() = channelInfo().relays?.mapNotNull { EventIdHint(id, it) } ?: emptyList() - fun channelInfo() = ChannelData.parse(content) ?: ChannelData() + fun channelInfo(): ChannelDataNorm { + return try { + ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() + } catch (e: JsonParseException) { + Log.e("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) + ChannelDataNorm() + } + } companion object { const val KIND = 40 @@ -52,13 +64,13 @@ class ChannelCreateEvent( name: String?, about: String?, picture: String?, - relays: List?, + relays: List?, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = build(ChannelData(name, about, picture, relays), createdAt, initializer) + ) = build(ChannelDataNorm(name, about, picture, relays), createdAt, initializer) fun build( - data: ChannelData, + data: ChannelDataNorm, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, data.toContent(), createdAt) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt index 13f7db101f..456bca8bc3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt @@ -20,16 +20,20 @@ */ package com.vitorpamplona.quartz.nip28PublicChat.admin +import android.util.Log import androidx.compose.runtime.Immutable +import com.fasterxml.jackson.core.JsonParseException import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip28PublicChat.base.BasePublicChatEvent import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData +import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm import com.vitorpamplona.quartz.nip28PublicChat.base.channel import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @@ -44,9 +48,16 @@ class ChannelMetadataEvent( sig: HexKey, ) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider { - override fun eventHints() = channelInfo().relays?.map { EventIdHint(id, it) } ?: emptyList() + override fun eventHints() = channelInfo().relays?.mapNotNull { EventIdHint(id, it) } ?: emptyList() - fun channelInfo() = ChannelData.parse(content) ?: ChannelData() + fun channelInfo(): ChannelDataNorm { + return try { + ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() + } catch (e: JsonParseException) { + Log.e("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) + ChannelDataNorm() + } + } companion object { const val KIND = 41 @@ -56,24 +67,24 @@ class ChannelMetadataEvent( name: String?, about: String?, picture: String?, - relays: List?, + relays: List?, channel: EventHintBundle, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = build(ChannelData(name, about, picture, relays), channel, createdAt, initializer) + ) = build(ChannelDataNorm(name, about, picture, relays), channel, createdAt, initializer) fun build( name: String?, about: String?, picture: String?, - relays: List?, + relays: List?, channel: ETag, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = build(ChannelData(name, about, picture, relays), channel, createdAt, initializer) + ) = build(ChannelDataNorm(name, about, picture, relays), channel, createdAt, initializer) fun build( - data: ChannelData, + data: ChannelDataNorm, channel: EventHintBundle, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, @@ -84,7 +95,7 @@ class ChannelMetadataEvent( } fun build( - data: ChannelData, + data: ChannelDataNorm, channel: ETag, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt index 088fed6f66..bdd0c2a0b6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt @@ -20,10 +20,11 @@ */ package com.vitorpamplona.quartz.nip28PublicChat.base -import android.util.Log import androidx.compose.runtime.Immutable import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer @Immutable data class ChannelData( @@ -34,15 +35,22 @@ data class ChannelData( ) { fun toContent() = assemble(this) + fun normalize() = ChannelDataNorm(name, about, picture, relays?.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }) + companion object { - fun parse(content: String): ChannelData? = - try { - EventMapper.mapper.readValue(content) - } catch (e: Exception) { - Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e) - ChannelData(null, null, null, null) - } + fun parse(content: String): ChannelData? = EventMapper.mapper.readValue(content) fun assemble(data: ChannelData) = EventMapper.mapper.writeValueAsString(data) } } + +data class ChannelDataNorm( + val name: String? = null, + val about: String? = null, + val picture: String? = null, + val relays: List? = null, +) { + fun denormalize() = ChannelData(name, about, picture, relays?.mapNotNull { it.url }) + + fun toContent() = denormalize().toContent() +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt index 727ee1a0ce..e2dbde88b1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -33,6 +34,7 @@ import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.map @Immutable class ChannelListEvent( @@ -99,7 +101,7 @@ class ChannelListEvent( ) fun createChannel( - channel: EventIdHint, + channel: EventIdHintOptional, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), @@ -113,7 +115,7 @@ class ChannelListEvent( ) fun createChannels( - channels: List, + channels: List, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), @@ -178,7 +180,7 @@ class ChannelListEvent( fun addChannel( earlierVersion: ChannelListEvent, - channel: EventIdHint, + channel: EventIdHintOptional, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), @@ -194,7 +196,7 @@ class ChannelListEvent( fun addChannels( earlierVersion: ChannelListEvent, - channels: List, + channels: List, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt index b297209a59..2c026c622e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt @@ -22,9 +22,10 @@ package com.vitorpamplona.quartz.nip28PublicChat.list import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.events.ETag fun TagArrayBuilder.followChat( eventId: HexKey, - relayUrl: String, + relayUrl: NormalizedRelayUrl, ) = addUnique(ETag.assemble(eventId, relayUrl, null)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt index 7cc031fb53..6b761c5142 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/repository/GitRepositoryEvent.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent.Companion.ALT_DESCRIPTION import com.vitorpamplona.quartz.nip34Git.repository.tags.CloneTag import com.vitorpamplona.quartz.nip34Git.repository.tags.DescriptionTag import com.vitorpamplona.quartz.nip34Git.repository.tags.NameTag @@ -54,7 +53,7 @@ class GitRepositoryEvent( companion object { const val KIND = 30617 - const val ALT = "Git Repository" + const val ALT_DESCRIPTION = "Git Repository" fun build( name: String, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningTag.kt index 5e451e384f..47c1a15c6e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/ContentWarningTag.kt @@ -20,26 +20,30 @@ */ package com.vitorpamplona.quartz.nip36SensitiveContent -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.arrayOfNotNull class ContentWarningTag( - val reason: String, + val reason: String? = null, ) { - fun countMemory(): Long = 1 * pointerSizeInBytes + reason.bytesUsedInMemory() - fun toTagArray() = assemble(reason) companion object { const val TAG_NAME = "content-warning" + @JvmStatic + fun isTag(tag: Array) = tag.has(0) && tag[0] == TAG_NAME + @JvmStatic fun parse(tags: Array): ContentWarningTag { require(tags[0] == TAG_NAME) - return ContentWarningTag(tags[1]) + return ContentWarningTag(tags.getOrNull(1)) } @JvmStatic - fun assemble(reason: String) = arrayOf(TAG_NAME, reason) + fun assemble() = arrayOfNotNull(TAG_NAME) + + @JvmStatic + fun assemble(reason: String?) = arrayOfNotNull(TAG_NAME, reason) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayBuilderExt.kt index 3698637340..fa44f876b7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayBuilderExt.kt @@ -23,4 +23,6 @@ package com.vitorpamplona.quartz.nip36SensitiveContent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +fun TagArrayBuilder.contentWarning() = add(ContentWarningTag.assemble()) + fun TagArrayBuilder.contentWarning(reason: String) = add(ContentWarningTag.assemble(reason)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayExt.kt index f9b1be1a63..e087a6aa89 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip36SensitiveContent/TagArrayExt.kt @@ -21,11 +21,10 @@ package com.vitorpamplona.quartz.nip36SensitiveContent import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag -fun TagArray.isSensitive() = this.any { (it.size > 0 && it[0] == ContentWarningTag.TAG_NAME) } +val nsfwTags = setOf("nsfw", "nude", "NSFW", "NUDE", "Nsfw", "Nude") -fun TagArray.isSensitiveOrNSFW() = - this.any { - (it.size > 0 && it[0] == ContentWarningTag.TAG_NAME) || - (it.size > 1 && it[0] == "t" && (it[1].equals("nsfw", true) || it[1].equals("nude", true))) - } +fun TagArray.isSensitive() = this.any(ContentWarningTag::isTag) + +fun TagArray.isSensitiveOrNSFW() = this.any { ContentWarningTag.isTag(it) || HashtagTag.isAnyTagged(it, nsfwTags) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt index d10fdc0bff..5e2d0dade2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt @@ -26,9 +26,18 @@ import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent @@ -45,7 +54,16 @@ class DraftEvent( tags: Array>, content: String, sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + @Transient private var cachedInnerEvent: Map = mapOf() override fun countMemory(): Long = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/EventExt.kt index a253366fe2..beb40d9ccb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/EventExt.kt @@ -21,15 +21,9 @@ package com.vitorpamplona.quartz.nip40Expiration import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.utils.TimeUtils -fun Event.expiration() = - try { - tags.firstOrNull { it.size > 1 && it[0] == "expiration" }?.get(1)?.toLongOrNull() - } catch (_: Exception) { - null - } +fun Event.expiration() = tags.expiration() -fun Event.isExpired() = (expiration() ?: Long.MAX_VALUE) < TimeUtils.now() +fun Event.isExpired() = tags.isExpired() -fun Event.isExpirationBefore(time: Long) = (expiration() ?: Long.MAX_VALUE) < time +fun Event.isExpirationBefore(time: Long) = tags.isExpirationBefore(time) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/ExpirationTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/ExpirationTag.kt new file mode 100644 index 0000000000..7e178f4517 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/ExpirationTag.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip40Expiration + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.ensure + +class ExpirationTag { + companion object { + const val TAG_NAME = "expiration" + + @JvmStatic + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun parse(tag: Array): Long? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1].toLongOrNull() + } + + @JvmStatic + fun assemble(time: Long) = arrayOfNotNull(TAG_NAME, time.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..2835041a82 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip40Expiration + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.expiration(time: Long) = addUnique(ExpirationTag.assemble(time)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayExt.kt new file mode 100644 index 0000000000..2ddaa8aa31 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayExt.kt @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip40Expiration + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.utils.TimeUtils + +fun TagArray.expiration() = this.firstNotNullOfOrNull(ExpirationTag::parse) + +fun TagArray.isExpired(): Boolean { + val exp = expiration() ?: return false + return exp < TimeUtils.now() +} + +fun TagArray.isExpirationBefore(time: Long): Boolean { + val exp = expiration() ?: return false + return exp < time +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt index e83a8bc1b6..83d8a17974 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt @@ -23,7 +23,10 @@ package com.vitorpamplona.quartz.nip42RelayAuth import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip42RelayAuth.tags.ChallengeTag +import com.vitorpamplona.quartz.nip42RelayAuth.tags.RelayTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -35,15 +38,15 @@ class RelayAuthEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - fun relay() = tags.firstOrNull { it.size > 1 && it[0] == "relay" }?.get(1) + fun relay() = tags.firstNotNullOfOrNull(RelayTag::parse) - fun challenge() = tags.firstOrNull { it.size > 1 && it[0] == "challenge" }?.get(1) + fun challenge() = tags.firstNotNullOfOrNull(ChallengeTag::parse) companion object { const val KIND = 22242 fun create( - relay: String, + relay: NormalizedRelayUrl, challenge: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), @@ -52,14 +55,14 @@ class RelayAuthEvent( val content = "" val tags = arrayOf( - arrayOf("relay", relay), - arrayOf("challenge", challenge), + RelayTag.assemble(relay), + ChallengeTag.assemble(challenge), ) signer.sign(createdAt, KIND, tags, content, onReady) } fun create( - relays: List, + relays: List, challenge: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), @@ -68,11 +71,9 @@ class RelayAuthEvent( val content = "" val tags = relays - .map { - arrayOf("relay", it) - }.plusElement( - arrayOf("challenge", challenge), - ).toTypedArray() + .map { RelayTag.assemble(it) } + .plusElement(ChallengeTag.assemble(challenge)) + .toTypedArray() signer.sign(createdAt, KIND, tags, content, onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/tags/ChallengeTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/tags/ChallengeTag.kt new file mode 100644 index 0000000000..2cf0d5fae5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/tags/ChallengeTag.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip42RelayAuth.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +class ChallengeTag { + companion object { + const val TAG_NAME = "challenge" + + @JvmStatic + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + @JvmStatic + fun assemble(url: String) = arrayOf(TAG_NAME, url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/tags/RelayTag.kt new file mode 100644 index 0000000000..bce30729f4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/tags/RelayTag.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip42RelayAuth.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.ensure + +class RelayTag { + companion object { + const val TAG_NAME = "relay" + + @JvmStatic + fun match(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun notMatch(tag: Array) = tag.has(0) && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): NormalizedRelayUrl? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null + + return relay + } + + @JvmStatic + fun assemble(relay: NormalizedRelayUrl) = arrayOf(TAG_NAME, relay.url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt index 2525ca7dcc..ef4bdbe987 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt @@ -23,13 +23,15 @@ package com.vitorpamplona.quartz.nip47WalletConnect import androidx.core.net.toUri import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import kotlinx.coroutines.CancellationException // Rename to the corect nip number when ready. class Nip47WalletConnect { companion object { - fun parse(uri: String): Nip47URI { + fun parse(uri: String): Nip47URINorm { // nostrwalletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&metadata=%7B%22name%22%3A%22Example%22%7D val url = uri.toUri() @@ -49,9 +51,10 @@ class Nip47WalletConnect { } val relay = url.getQueryParameter("relay") ?: throw IllegalArgumentException("Relay cannot be null") + val relayNorm = RelayUrlNormalizer.normalizeOrNull(relay) ?: throw IllegalArgumentException("Invalid relay Url") val secret = url.getQueryParameter("secret") - return Nip47URI(pubkeyHex, relay, secret) + return Nip47URINorm(pubkeyHex, relayNorm, secret) } } @@ -59,5 +62,20 @@ class Nip47WalletConnect { val pubKeyHex: HexKey, val relayUri: String, val secret: HexKey?, + ) { + fun normalize(): Nip47WalletConnect.Nip47URINorm? = + RelayUrlNormalizer.normalizeOrNull(relayUri)?.let { + Nip47URINorm( + pubKeyHex, + it, + secret, + ) + } + } + + data class Nip47URINorm( + val pubKeyHex: HexKey, + val relayUri: NormalizedRelayUrl, + val secret: HexKey?, ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt index b43cdf53fb..8ef77d9f48 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt @@ -23,11 +23,13 @@ package com.vitorpamplona.quartz.nip50Search import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip50Search.tags.RelayTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -39,14 +41,7 @@ class SearchRelayListEvent( content: String, sig: HexKey, ) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun relays(): List = - tags.mapNotNull { - if (it.size > 1 && it[0] == "relay") { - it[1] - } else { - null - } - } + fun relays(): List = tags.mapNotNull(RelayTag::parse) companion object { const val KIND = 10007 @@ -57,26 +52,26 @@ class SearchRelayListEvent( fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) - fun createTagArray(relays: List): Array> = + fun createTagArray(relays: List): Array> = relays .map { - arrayOf("relay", it) + RelayTag.assemble(it) }.plusElement(AltTag.assemble("Relay list to use for Search")) .toTypedArray() fun updateRelayList( earlierVersion: SearchRelayListEvent, - relays: List, + relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (SearchRelayListEvent) -> Unit, ) { val tags = earlierVersion.tags - .filter { it[0] != "relay" } + .filter(RelayTag::notMatch) .plus( relays.map { - arrayOf("relay", it) + RelayTag.assemble(it) }, ).toTypedArray() @@ -84,7 +79,7 @@ class SearchRelayListEvent( } fun createFromScratch( - relays: List, + relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (SearchRelayListEvent) -> Unit, @@ -93,7 +88,7 @@ class SearchRelayListEvent( } fun create( - relays: List, + relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (SearchRelayListEvent) -> Unit, @@ -102,7 +97,7 @@ class SearchRelayListEvent( } fun create( - relays: List, + relays: List, signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), ): SearchRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "") diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/tags/RelayTag.kt new file mode 100644 index 0000000000..db8babc105 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/tags/RelayTag.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip50Search.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.ensure + +class RelayTag { + companion object { + const val TAG_NAME = "relay" + + @JvmStatic + fun match(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun notMatch(tag: Array) = tag.has(0) && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): NormalizedRelayUrl? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null + + return relay + } + + @JvmStatic + fun assemble(relay: NormalizedRelayUrl) = arrayOf(TAG_NAME, relay.url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt index fb56d48f8a..4536f28bd2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt @@ -25,7 +25,10 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent.UsersAndWords import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlin.coroutines.resume @Immutable class MuteListEvent( @@ -38,17 +41,34 @@ class MuteListEvent( ) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { override fun dTag() = FIXED_D_TAG + fun publicUsersAndWords() = + PeopleListEvent.UsersAndWords( + filterTagList("p", tags), + filterTagList("word", tags), + ) + fun publicAndPrivateUsersAndWords( signer: NostrSigner, onReady: (PeopleListEvent.UsersAndWords) -> Unit, ) { privateTagsOrEmpty(signer) { onReady( - PeopleListEvent.UsersAndWords(filterTagList("p", it), filterTagList("word", it)), + PeopleListEvent.UsersAndWords( + filterTagList("p", it), + filterTagList("word", it), + ), ) } } + suspend fun publicAndPrivateUsersAndWords(signer: NostrSigner): UsersAndWords? { + return tryAndWait { continuation -> + publicAndPrivateUsersAndWords(signer) { privateTagList -> + continuation.resume(privateTagList) + } + } + } + companion object { const val KIND = 10000 const val FIXED_D_TAG = "" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt index 568c7fbddd..3e35ff9f55 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt @@ -26,6 +26,8 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlin.coroutines.resume @Immutable class PeopleListEvent( @@ -42,6 +44,12 @@ class PeopleListEvent( val words: Set = setOf(), ) + fun publicUsersAndWords() = + UsersAndWords( + filterTagList("p", tags), + filterTagList("word", tags), + ) + fun publicAndPrivateUsersAndWords( signer: NostrSigner, onReady: (UsersAndWords) -> Unit, @@ -56,6 +64,14 @@ class PeopleListEvent( } } + suspend fun publicAndPrivateUsersAndWords(signer: NostrSigner): UsersAndWords? { + return tryAndWait { continuation -> + publicAndPrivateUsersAndWords(signer) { privateTagList -> + continuation.resume(privateTagList) + } + } + } + fun isTaggedWord( word: String, isPrivate: Boolean, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt index b43207bf4e..4a21116809 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt @@ -23,8 +23,10 @@ package com.vitorpamplona.quartz.nip51Lists import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip51Lists.tags.RelayTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -36,7 +38,7 @@ class RelaySetEvent( content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun relays() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } + fun relays(): List = tags.mapNotNull(RelayTag::parse) fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt new file mode 100644 index 0000000000..d56059e18c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.interests + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlin.coroutines.resume + +@Immutable +class HashtagListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + @Transient var publicAndPrivateHashtagCache: Set? = null + + fun publicHashtags() = tags.mapNotNull(HashtagTag::parse) + + fun publicAndPrivateHashtag( + signer: NostrSigner, + onReady: (Set) -> Unit, + ) { + publicAndPrivateHashtagCache?.let { eventList -> + onReady(eventList) + return + } + + mergeTagList(signer) { + val set = it.mapNotNull(HashtagTag::parse).toSet() + publicAndPrivateHashtagCache = set + onReady(set) + } + } + + suspend fun publicAndPrivateHashtag(signer: NostrSigner): Set? { + publicAndPrivateHashtagCache?.let { return it } + + return tryAndWait { continuation -> + publicAndPrivateHashtag(signer) { privateTagList -> + continuation.resume(privateTagList) + } + } + } + + companion object { + const val KIND = 10015 + const val ALT = "Hashtag List" + const val FIXED_D_TAG = "" + + fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) + + private fun createHashtagBase( + tags: Array>, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (HashtagListEvent) -> Unit, + ) { + PrivateTagArrayBuilder.create( + tags, + isPrivate, + signer, + ) { encryptedContent, newTags -> + create(encryptedContent, newTags, signer, createdAt, onReady) + } + } + + fun createHashtag( + hashtag: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (HashtagListEvent) -> Unit, + ) = createHashtagBase( + tags = arrayOf(HashtagTag.assemble(hashtag)), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + + fun createHashtags( + hashtags: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (HashtagListEvent) -> Unit, + ) = createHashtagBase( + tags = HashtagTag.assemble(hashtags).toTypedArray(), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + + fun removeHashtag( + earlierVersion: HashtagListEvent, + hashtag: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (HashtagListEvent) -> Unit, + ) { + PrivateTagArrayBuilder.removeAll( + earlierVersion, + HashtagTag.assemble(hashtag), + signer, + ) { encryptedContent, newTags -> + create(encryptedContent, newTags, signer, createdAt, onReady) + } + } + + private fun addHashtagBase( + earlierVersion: HashtagListEvent, + newTags: Array>, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (HashtagListEvent) -> Unit, + ) { + PrivateTagArrayBuilder.addAll( + earlierVersion, + newTags, + isPrivate, + signer, + ) { encryptedContent, newTags -> + create(encryptedContent, newTags, signer, createdAt, onReady) + } + } + + fun addHashtag( + earlierVersion: HashtagListEvent, + hashtag: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (HashtagListEvent) -> Unit, + ) = addHashtagBase( + earlierVersion, + arrayOf(HashtagTag.assemble(hashtag)), + isPrivate, + signer, + createdAt, + onReady, + ) + + fun addHashtags( + earlierVersion: HashtagListEvent, + hashtags: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (HashtagListEvent) -> Unit, + ) = addHashtagBase( + earlierVersion, + HashtagTag.assemble(hashtags).toTypedArray(), + isPrivate, + signer, + createdAt, + onReady, + ) + + private fun create( + content: String, + tags: Array>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (HashtagListEvent) -> Unit, + ) { + val newTags = + if (tags.any { it.size > 1 && it[0] == "alt" }) { + tags + } else { + tags + AltTag.assemble(ALT) + } + + signer.sign(createdAt, KIND, newTags, content, onReady) + } + + fun create( + hashtags: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): HashtagListEvent? { + val tags = HashtagTag.assemble(hashtags).toTypedArray() + return signer.sign(createdAt, KIND, tags, "") + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..4fca44c0e3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.interests + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag + +fun TagArrayBuilder.followHashTag(hashtag: String) = add(HashtagTag.assemble(hashtag)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt new file mode 100644 index 0000000000..dff171129f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.locations + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag.Companion.parse +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlin.coroutines.resume + +@Immutable +class GeohashListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + @Transient var publicAndPrivateGeohashCache: Set? = null + + fun publicGeohashes() = tags.mapNotNull(GeoHashTag::parse) + + fun publicAndPrivateGeohash( + signer: NostrSigner, + onReady: (Set) -> Unit, + ) { + publicAndPrivateGeohashCache?.let { eventList -> + onReady(eventList) + return + } + + mergeTagList(signer) { + val set = it.mapNotNull(GeoHashTag::parse).toSet() + publicAndPrivateGeohashCache = set + onReady(set) + } + } + + suspend fun publicAndPrivateGeohash(signer: NostrSigner): Set? { + publicAndPrivateGeohashCache?.let { return it } + + return tryAndWait { continuation -> + publicAndPrivateGeohash(signer) { privateTagList -> + continuation.resume(privateTagList) + } + } + } + + companion object { + const val KIND = 10081 + const val ALT = "Geohash List" + const val FIXED_D_TAG = "" + + fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) + + private fun createGeohashBase( + tags: Array>, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GeohashListEvent) -> Unit, + ) { + PrivateTagArrayBuilder.create( + tags, + isPrivate, + signer, + ) { encryptedContent, newTags -> + create(encryptedContent, newTags, signer, createdAt, onReady) + } + } + + fun createGeohash( + geohash: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GeohashListEvent) -> Unit, + ) = createGeohashBase( + tags = arrayOf(GeoHashTag.assembleSingle(geohash)), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + + fun createGeohashs( + geohashs: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GeohashListEvent) -> Unit, + ) = createGeohashBase( + tags = geohashs.map { GeoHashTag.assembleSingle(it) }.toTypedArray(), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + + fun removeGeohash( + earlierVersion: GeohashListEvent, + geohash: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GeohashListEvent) -> Unit, + ) { + PrivateTagArrayBuilder.removeAll( + earlierVersion, + GeoHashTag.assembleSingle(geohash), + signer, + ) { encryptedContent, newTags -> + create(encryptedContent, newTags, signer, createdAt, onReady) + } + } + + private fun addGeohashBase( + earlierVersion: GeohashListEvent, + newTags: Array>, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GeohashListEvent) -> Unit, + ) { + PrivateTagArrayBuilder.addAll( + earlierVersion, + newTags, + isPrivate, + signer, + ) { encryptedContent, newTags -> + create(encryptedContent, newTags, signer, createdAt, onReady) + } + } + + fun addGeohash( + earlierVersion: GeohashListEvent, + geohash: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GeohashListEvent) -> Unit, + ) = addGeohashBase( + earlierVersion, + arrayOf(GeoHashTag.assembleSingle(geohash)), + isPrivate, + signer, + createdAt, + onReady, + ) + + fun addGeohashs( + earlierVersion: GeohashListEvent, + geohashs: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GeohashListEvent) -> Unit, + ) = addGeohashBase( + earlierVersion, + geohashs.map { GeoHashTag.assembleSingle(it) }.toTypedArray(), + isPrivate, + signer, + createdAt, + onReady, + ) + + private fun create( + content: String, + tags: Array>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (GeohashListEvent) -> Unit, + ) { + val newTags = + if (tags.any { it.size > 1 && it[0] == "alt" }) { + tags + } else { + tags + AltTag.assemble(ALT) + } + + signer.sign(createdAt, KIND, newTags, content, onReady) + } + + fun create( + geohashs: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): GeohashListEvent? { + val tags = geohashs.map { GeoHashTag.assembleSingle(it) }.toTypedArray() + return signer.sign(createdAt, KIND, tags, "") + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..e91af0ce2f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.locations + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag + +fun TagArrayBuilder.followGeohash(geohash: String) = add(GeoHashTag.assembleSingle(geohash)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/RelayTag.kt new file mode 100644 index 0000000000..50b700f645 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/RelayTag.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.ensure + +class RelayTag { + companion object { + const val TAG_NAME = "r" + + @JvmStatic + fun match(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun notMatch(tag: Array) = tag.has(0) && tag[0] == TAG_NAME + + @JvmStatic + fun parse(tag: Array): NormalizedRelayUrl? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null + + return relay + } + + @JvmStatic + fun assemble(relay: NormalizedRelayUrl) = arrayOf(TAG_NAME, relay.url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt index e72556a15c..df9a1dc4c2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt @@ -24,7 +24,18 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag @@ -38,6 +49,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StreamingTag import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.TotalParticipantsTag import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.flatten @Immutable class LiveActivitiesEvent( @@ -47,7 +59,16 @@ class LiveActivitiesEvent( tags: Array>, content: String, sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + + override fun pubKeyHints() = tags.mapNotNull(ParticipantTag::parseAsHint) + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) @@ -70,7 +91,7 @@ class LiveActivitiesEvent( fun relays() = tags.mapNotNull(RelayListTag::parse) - fun allRelayUrls() = tags.mapNotNull(RelayListTag::parse).map { it.relayUrls }.flatten() + fun allRelayUrls() = tags.mapNotNull(RelayListTag::parse).flatten() fun hasHost() = tags.any(ParticipantTag::isHost) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/ParticipantTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/ParticipantTag.kt index 1c2733fbe1..5478fedf1b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/ParticipantTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/ParticipantTag.kt @@ -24,6 +24,9 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.ensure @@ -38,7 +41,7 @@ enum class ROLE( @Immutable data class ParticipantTag( override val pubKey: String, - override val relayHint: String?, + override val relayHint: NormalizedRelayUrl?, val role: String?, val proof: String?, ) : PubKeyReferenceTag { @@ -64,7 +67,10 @@ data class ParticipantTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return ParticipantTag(tag[1], tag.getOrNull(2), tag.getOrNull(3), tag.getOrNull(4)) + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return ParticipantTag(tag[1], hint, tag.getOrNull(3), tag.getOrNull(4)) } @JvmStatic @@ -73,7 +79,10 @@ data class ParticipantTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[3] == ROLE.HOST.code) { return null } - return ParticipantTag(tag[1], tag[2], tag[3], tag.getOrNull(4)) + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + + return ParticipantTag(tag[1], hint, tag[3], tag.getOrNull(4)) } @JvmStatic @@ -84,6 +93,19 @@ data class ParticipantTag( return tag[1] } + @JvmStatic + fun parseAsHint(tag: Array): PubKeyHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(hint != null) { return null } + + return PubKeyHint(tag[1], hint) + } + @JvmStatic fun assemble( pubkey: HexKey, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/RelayListTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/RelayListTag.kt index ac25970480..e158c766f8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/RelayListTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/RelayListTag.kt @@ -21,30 +21,30 @@ package com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.ensure -class RelayListTag( - val relayUrls: List, -) { +class RelayListTag { companion object { const val TAG_NAME = "relays" @JvmStatic - fun parse(tag: Array): RelayListTag? { + fun parse(tag: Array): List? { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } val relays = tag.mapIndexedNotNull { index, s -> - if (index == 0) null else s + if (index == 0) null else RelayUrlNormalizer.normalizeOrNull(s) } - return RelayListTag(relays) + + if (relays.isEmpty()) return null + + return relays } @JvmStatic - fun assemble(urls: List) = arrayOf(TAG_NAME) + urls.toTypedArray() - - @JvmStatic - fun assemble(tag: RelayListTag) = assemble(tag.relayUrls) + fun assemble(urls: List) = arrayOf(TAG_NAME) + urls.map { it.url }.toTypedArray() } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index 057fd9b2d7..823edee0d6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip54Wiki import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -44,7 +45,7 @@ class WikiNoteEvent( AddressableEvent { override fun dTag() = tags.dTag() - override fun aTag(relayHint: String?) = ATag(kind, pubKey, dTag(), relayHint) + override fun aTag(relayHint: NormalizedRelayUrl?) = ATag(kind, pubKey, dTag(), relayHint) override fun address() = Address(kind, pubKey, dTag()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt index e294e32f87..2b7d9ad680 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt @@ -33,6 +33,8 @@ class NostrSignerExternal( pubKey: HexKey, val launcher: ExternalSignerLauncher, ) : NostrSigner(pubKey) { + override fun isWriteable(): Boolean = true + override fun sign( createdAt: Long, kind: Int, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt index 099ea723a7..c6876c9bbf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt @@ -24,8 +24,17 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip56Reports.tags.DefaultReportTag @@ -43,7 +52,16 @@ class ReportEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + @Transient private var defaultType: ReportType? = null diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt index 21403f5174..28c669c91e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt @@ -20,12 +20,22 @@ */ package com.vitorpamplona.quartz.nip57Zaps +import android.R.attr.description import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable @@ -37,7 +47,16 @@ class LnZapEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig), - LnZapEventInterface { + LnZapEventInterface, + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + // This event is also kept in LocalCache (same object) @Transient val zapRequest: LnZapRequestEvent? diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt index 60aaae691e..071f95c149 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt @@ -23,8 +23,17 @@ package com.vitorpamplona.quartz.nip57Zaps import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -35,7 +44,16 @@ class LnZapPrivateEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + companion object { const val KIND = 9733 const val ALT = "Private zap" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt index 2f4dcd910e..8df6711324 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt @@ -27,8 +27,13 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip31Alts.AltTag @@ -43,7 +48,16 @@ class LnZapRequestEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + @Transient private var privateZapEvent: LnZapPrivateEvent? = null override fun countMemory(): Long = super.countMemory() + pointerSizeInBytes + (privateZapEvent?.countMemory() ?: 0) @@ -138,7 +152,7 @@ class LnZapRequestEvent( fun create( userHex: String, - relays: Set, + relays: Set, signer: NostrSigner, message: String, zapType: LnZapEvent.ZapType, @@ -150,7 +164,7 @@ class LnZapRequestEvent( var tags = arrayOf( arrayOf("p", userHex), - arrayOf("relays") + relays, + arrayOf("relays") + relays.map { it.url }, ) if (zapType == LnZapEvent.ZapType.ANONYMOUS) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetup.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetup.kt index dd33eb64fb..1f48c9a106 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetup.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetup.kt @@ -21,10 +21,11 @@ package com.vitorpamplona.quartz.nip57Zaps.splits import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl data class ZapSplitSetup( val pubKeyHex: HexKey, - val relay: String?, + val relay: NormalizedRelayUrl?, override val weight: Double, ) : BaseZapSplitSetup { override fun mainId() = pubKeyHex diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupParser.kt index 4d12cca53b..3924e101b9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupParser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupParser.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip57Zaps.splits import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.ensure class ZapSplitSetupParser { @@ -40,9 +41,11 @@ class ZapSplitSetupParser { if (isLnAddress) { ZapSplitSetupLnAddress(tags[1], 1.0) } else { + val relayHint = tags.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + ZapSplitSetup( tags[1], - tags.getOrNull(2), + relayHint, weight, ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupSerializer.kt index 81a93f2473..dbef78241f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/splits/ZapSplitSetupSerializer.kt @@ -20,13 +20,15 @@ */ package com.vitorpamplona.quartz.nip57Zaps.splits +import com.vitorpamplona.quartz.utils.arrayOfNotNull + class ZapSplitSetupSerializer { companion object { @JvmStatic fun toTagArray(zapSplit: BaseZapSplitSetup): Array = when (zapSplit) { is ZapSplitSetupLnAddress -> arrayOf(BaseZapSplitSetup.TAG_NAME, zapSplit.lnAddress) - is ZapSplitSetup -> arrayOf(BaseZapSplitSetup.TAG_NAME, zapSplit.pubKeyHex, zapSplit.relay ?: "", zapSplit.weight.toString()) + is ZapSplitSetup -> arrayOfNotNull(BaseZapSplitSetup.TAG_NAME, zapSplit.pubKeyHex, zapSplit.relay?.url, zapSplit.weight.toString()) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt index 8ead7f1c2e..be7996a23a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt @@ -23,7 +23,16 @@ package com.vitorpamplona.quartz.nip58Badges import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers @@ -35,7 +44,16 @@ class BadgeAwardEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + fun awardees() = taggedUsers() fun awardeeIds() = taggedUserIds() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt index edd9ee6951..9488881d92 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt @@ -23,10 +23,17 @@ package com.vitorpamplona.quartz.nip58Badges import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint @Immutable class BadgeProfilesEvent( @@ -36,7 +43,16 @@ class BadgeProfilesEvent( tags: Array>, content: String, sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + fun badgeAwardEvents() = taggedEvents() fun badgeAwardDefinitions() = taggedAddresses() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt index ab53f0a072..362758f780 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt @@ -27,8 +27,9 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.map @Immutable class AdvertisedRelayListEvent( @@ -39,48 +40,15 @@ class AdvertisedRelayListEvent( content: String, sig: HexKey, ) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun relays(): List = - tags.mapNotNull { - if (it.size > 1 && it[0] == "r") { - val type = - when (it.getOrNull(2)) { - "read" -> AdvertisedRelayType.READ - "write" -> AdvertisedRelayType.WRITE - else -> AdvertisedRelayType.BOTH - } + fun relays() = tags.mapNotNull(AdvertisedRelayInfo::parse) - AdvertisedRelayInfo(it[1], type) - } else { - null - } - } + fun readRelays() = tags.mapNotNull(AdvertisedRelayInfo::parseRead).ifEmpty { null } - fun readRelays(): List? = - tags - .mapNotNull { - if (it.size > 1 && it[0] == "r") { - when (it.getOrNull(2)) { - "read" -> it[1] - "write" -> null - else -> it[1] - } - } else { - null - } - }.ifEmpty { null } + fun readRelaysNorm() = tags.mapNotNull(AdvertisedRelayInfo::parseReadNorm).ifEmpty { null } - fun writeRelays(): List = - tags.mapNotNull { - if (it.size > 1 && it[0] == "r") { - when (it.getOrNull(2)) { - "read" -> null - "write" -> it[1] - else -> it[1] - } - } else { - null - } - } + fun writeRelays() = tags.mapNotNull(AdvertisedRelayInfo::parseWrite).ifEmpty { null } + + fun writeRelaysNorm() = tags.mapNotNull(AdvertisedRelayInfo::parseWriteNorm).ifEmpty { null } companion object { const val KIND = 10002 @@ -92,21 +60,17 @@ class AdvertisedRelayListEvent( fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) - fun updateRelayList( + fun replaceRelayListWith( earlierVersion: AdvertisedRelayListEvent, - relays: List, + newRelays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (AdvertisedRelayListEvent) -> Unit, ) { - val tags = - earlierVersion.tags - .filter { it[0] != "r" } - .plus( - relays.map(Companion::createRelayTag), - ).toTypedArray() + val tags = earlierVersion.tags.filter(AdvertisedRelayInfo::notMatch) + val relayTags = newRelays.map { it.toTagArray() } - signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady) + signer.sign(createdAt, KIND, (tags + relayTags).toTypedArray(), earlierVersion.content, onReady) } fun createFromScratch( @@ -114,22 +78,7 @@ class AdvertisedRelayListEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (AdvertisedRelayListEvent) -> Unit, - ) { - create(relays, signer, createdAt, onReady) - } - - fun createRelayTag(relay: AdvertisedRelayInfo): Array = - if (relay.type == AdvertisedRelayType.BOTH) { - arrayOf("r", relay.relayUrl) - } else { - arrayOf("r", relay.relayUrl, relay.type.code) - } - - fun createTagArray(relays: List): Array> = - relays - .map(Companion::createRelayTag) - .plusElement(AltTag.assemble(ALT)) - .toTypedArray() + ) = create(relays, signer, createdAt, onReady) fun create( list: List, @@ -137,7 +86,7 @@ class AdvertisedRelayListEvent( createdAt: Long = TimeUtils.now(), onReady: (AdvertisedRelayListEvent) -> Unit, ) { - val tags = createTagArray(list) + val tags = list.map { it.toTagArray() }.toTypedArray() val msg = "" signer.sign(createdAt, KIND, tags, msg, onReady) @@ -148,24 +97,10 @@ class AdvertisedRelayListEvent( signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), ): AdvertisedRelayListEvent? { - val tags = createTagArray(list) + val tags = list.map { it.toTagArray() }.toTypedArray() val msg = "" return signer.sign(createdAt, KIND, tags, msg) } } - - @Immutable data class AdvertisedRelayInfo( - val relayUrl: String, - val type: AdvertisedRelayType, - ) - - @Immutable - enum class AdvertisedRelayType( - val code: String, - ) { - BOTH(""), - READ("read"), - WRITE("write"), - } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt index 88be891cf8..fd0953e566 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt @@ -21,29 +21,26 @@ package com.vitorpamplona.quartz.nip65RelayList import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isOnion +import com.vitorpamplona.quartz.utils.mapOfSet class RelayListRecommendationProcessor { companion object { fun transpose( - userList: Map>, - ignore: Set = setOf(), - ): Map> { - val popularity = mutableMapOf>() - - userList.forEach { event -> - event.value.forEach { relayUrl -> - if (relayUrl !in ignore) { - val set = popularity[relayUrl] - if (set != null) { - set.add(event.key) - } else { - popularity[relayUrl] = mutableSetOf(event.key) + userList: Map>, + ignore: Set = setOf(), + ): Map> { + return mapOfSet { + userList.forEach { event -> + event.value.forEach { relay -> + if (relay !in ignore) { + add(relay, event.key) } } } } - - return popularity } /** @@ -53,30 +50,21 @@ class RelayListRecommendationProcessor { private fun filterValidRelays( userList: List, hasOnionConnection: Boolean = false, - ): MutableMap> { - val validWriteRelayUrls = mutableMapOf>() - - userList.forEach { event -> - event.writeRelays().forEach { relayUrl -> - if (!RelayUrlFormatter.isLocalHost(relayUrl) && (hasOnionConnection || !RelayUrlFormatter.isOnion(relayUrl))) { - RelayUrlFormatter.normalizeOrNull(relayUrl)?.let { normRelayUrl -> - val set = validWriteRelayUrls[event.pubKey] - if (set != null) { - set.add(normRelayUrl) - } else { - validWriteRelayUrls[event.pubKey] = mutableSetOf(normRelayUrl) - } + ): Map> { + return mapOfSet { + userList.forEach { event -> + event.writeRelaysNorm()?.forEach { relay -> + if (!relay.isLocalHost() && (hasOnionConnection || !relay.isOnion())) { + add(event.pubKey, relay) } } } } - - return validWriteRelayUrls } fun reliableRelaySetFor( - usersAndRelays: MutableMap>, - relayUrlsToIgnore: Set = emptySet(), + usersAndRelays: Map>, + relayUrlsToIgnore: Set = emptySet(), ): Set { // ignores users that are already being served by the list. val usersToServeInTheFirstRound = @@ -145,7 +133,7 @@ class RelayListRecommendationProcessor { fun reliableRelaySetFor( userList: List, - relayUrlsToIgnore: Set = emptySet(), + relayUrlsToIgnore: Set = emptySet(), hasOnionConnection: Boolean = false, ): Set = reliableRelaySetFor( @@ -155,7 +143,7 @@ class RelayListRecommendationProcessor { } class RelayRecommendation( - val url: String, + val relay: NormalizedRelayUrl, val requiredToNotMissEvents: Boolean, val users: Set, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayUrlFormatter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayUrlFormatter.kt deleted file mode 100644 index fdfeb3bf6a..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayUrlFormatter.kt +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip65RelayList - -import org.czeal.rfc3986.URIReference - -class RelayUrlFormatter { - companion object { - fun displayUrl(url: String): String = - url - .trim() - .removePrefix("wss://") - .removePrefix("ws://") - .removeSuffix("/") - - fun isLocalHost(url: String) = url.contains("127.0.0.1") || url.contains("localhost") - - fun isOnion(url: String) = url.endsWith(".onion") || url.endsWith(".onion/") - - fun normalize(url: String): String { - val newUrl = - if (!url.startsWith("wss://") && !url.startsWith("ws://")) { - if (isOnion(url) || isLocalHost(url)) { - "ws://${url.trim()}" - } else { - "wss://${url.trim()}" - } - } else { - url.trim() - } - - return try { - URIReference.parse(newUrl).normalize().toString() - } catch (e: Exception) { - newUrl - } - } - - fun normalizeOrNull(url: String): String? { - val newUrl = - if (!url.startsWith("wss://") && !url.startsWith("ws://")) { - if (isOnion(url) || isLocalHost(url)) { - "ws://${url.trim()}" - } else { - "wss://${url.trim()}" - } - } else { - url.trim() - } - - return try { - URIReference.parse(newUrl).normalize().toString() - } catch (e: Exception) { - null - } - } - - fun getHttpsUrl(dirtyUrl: String): String = - if (dirtyUrl.contains("://")) { - dirtyUrl.replace("wss://", "https://").replace("ws://", "http://") - } else { - "https://$dirtyUrl" - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt new file mode 100644 index 0000000000..07d6a13194 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip65RelayList.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.ensure +import kotlin.text.isBlank + +class AdvertisedRelayInfo( + val relayUrl: NormalizedRelayUrl, + val type: AdvertisedRelayType, +) { + fun toTagArray() = assemble(relayUrl, type) + + companion object { + const val TAG_NAME = "r" + + fun match(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun notMatch(tag: Array) = !match(tag) + + @JvmStatic + fun parse(tag: Array): AdvertisedRelayInfo? { + ensure(match(tag)) { return null } + + val normalizedUrl = RelayUrlNormalizer.normalizeOrNull(tag[1]) + + ensure(normalizedUrl != null) { return null } + + val type = + when (tag.getOrNull(2)) { + AdvertisedRelayType.READ.code -> AdvertisedRelayType.READ + AdvertisedRelayType.WRITE.code -> AdvertisedRelayType.WRITE + else -> AdvertisedRelayType.BOTH + } + + return AdvertisedRelayInfo(normalizedUrl, type) + } + + @JvmStatic + fun parseRead(tag: Array): String? { + ensure(match(tag)) { return null } + ensure(AdvertisedRelayType.isRead(tag.getOrNull(2))) { return null } + + return tag[1] + } + + @JvmStatic + fun parseWrite(tag: Array): String? { + ensure(match(tag)) { return null } + ensure(AdvertisedRelayType.isWrite(tag.getOrNull(2))) { return null } + + return tag[1] + } + + @JvmStatic + fun parseReadNorm(tag: Array): NormalizedRelayUrl? { + ensure(match(tag)) { return null } + ensure(AdvertisedRelayType.isRead(tag.getOrNull(2))) { return null } + + return RelayUrlNormalizer.normalizeOrNull(tag[1]) + } + + @JvmStatic + fun parseWriteNorm(tag: Array): NormalizedRelayUrl? { + ensure(match(tag)) { return null } + ensure(AdvertisedRelayType.isWrite(tag.getOrNull(2))) { return null } + + return RelayUrlNormalizer.normalizeOrNull(tag[1]) + } + + @JvmStatic + fun assemble( + relay: NormalizedRelayUrl, + type: AdvertisedRelayType, + ): Array { + return if (type == AdvertisedRelayType.BOTH) { + arrayOf(TAG_NAME, relay.url) + } else { + arrayOf(TAG_NAME, relay.url, type.code) + } + } + } +} + +@Immutable +enum class AdvertisedRelayType( + val code: String, +) { + BOTH(""), + READ("read"), + WRITE("write"), + ; + + fun isRead() = this == READ || this == BOTH + + fun isWrite() = this == WRITE || this == BOTH + + companion object { + fun isRead(type: String?) = type == null || type.isBlank() || type == AdvertisedRelayType.READ.code + + fun isWrite(type: String?) = type == null || type.isBlank() || type == AdvertisedRelayType.WRITE.code + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityListEvent.kt deleted file mode 100644 index f304c1283f..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/CommunityListEvent.kt +++ /dev/null @@ -1,279 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip72ModCommunities - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent.Companion.FIXED_D_TAG -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip19Bech32.parseAtag -import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent -import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.pointerSizeInBytes -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.toImmutableSet - -@Immutable -class CommunityListEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient var publicAndPrivateEventCache: ImmutableSet? = null - - override fun countMemory(): Long = - super.countMemory() + - 32 + (publicAndPrivateEventCache?.sumOf { pointerSizeInBytes + it.countMemory() } ?: 0L) // rough calculation - - override fun dTag() = FIXED_D_TAG - - fun publicAndPrivateEvents( - signer: NostrSigner, - onReady: (ImmutableSet) -> Unit, - ) { - publicAndPrivateEventCache?.let { eventList -> - onReady(eventList) - return - } - - privateTagsOrEmpty(signer) { - publicAndPrivateEventCache = - filterTagList("a", it) - .mapNotNull { ATag.parseAtag(it, null) } - .toImmutableSet() - - publicAndPrivateEventCache?.let { eventList -> - onReady(eventList) - } - } - } - - companion object { - const val KIND = 10004 - const val ALT = "Community List" - - fun blockListFor(pubKeyHex: HexKey): String = "$KIND:$pubKeyHex:" - - fun createListWithTag( - key: String, - tag: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) { - if (isPrivate) { - encryptTags(arrayOf(arrayOf(key, tag)), signer) { encryptedTags -> - create( - content = encryptedTags, - tags = emptyArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } else { - create( - content = "", - tags = arrayOf(arrayOf(key, tag)), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - - fun createListWithEvent( - address: ATag, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) = createListWithTag("a", address.toTag(), isPrivate, signer, createdAt, onReady) - - fun addEvents( - earlierVersion: CommunityListEvent, - listAddresses: List, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = - privateTags.plus( - listAddresses.map { arrayOf("a", it.toTag()) }, - ), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = earlierVersion.tags, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = - earlierVersion.tags.plus( - listAddresses.map { arrayOf("a", it.toTag()) }, - ), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - - fun addEvent( - earlierVersion: CommunityListEvent, - address: ATag, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) = addTag(earlierVersion, "a", address.toTag(), isPrivate, signer, createdAt, onReady) - - fun addTag( - earlierVersion: CommunityListEvent, - key: String, - tag: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) { - earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> - if (!isTagged) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = privateTags.plus(element = arrayOf(key, tag)), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = earlierVersion.tags, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = earlierVersion.tags.plus(element = arrayOf(key, tag)), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } - } - - fun removeEvent( - earlierVersion: CommunityListEvent, - address: ATag, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) = removeTag(earlierVersion, "a", address.toTag(), isPrivate, signer, createdAt, onReady) - - fun removeTag( - earlierVersion: CommunityListEvent, - key: String, - tag: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) { - earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> - if (isTagged) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = - privateTags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = - earlierVersion.tags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = - earlierVersion.tags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } - } - - fun create( - content: String, - tags: Array>, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) { - val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { - tags - } else { - tags + AltTag.assemble(ALT) - } - - signer.sign(createdAt, KIND, newTags, content, onReady) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt index 5b404a33d3..726a0b8193 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt @@ -25,12 +25,24 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -43,7 +55,16 @@ class CommunityPostApprovalEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + fun containedPost(): Event? = try { content.ifBlank { null }?.let { fromJson(it) } @@ -68,6 +89,7 @@ class CommunityPostApprovalEvent( companion object { const val KIND = 4550 const val ALT_DESCRIPTION = "Community post approval" + val KIND_LIST = listOf(CommunityPostApprovalEvent.KIND) fun build( approvedPost: EventHintBundle, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt index 0498313350..51feebe0d9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt @@ -24,8 +24,20 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.DescriptionTag import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ImageTag @@ -44,7 +56,19 @@ class CommunityDefinitionEvent( tags: Array>, content: String, sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + + override fun addressHints() = + tags.mapNotNull(ATag::parseAsHint) + + tags.mapNotNull(QTag::parseAddressAsHint) + + tags.mapNotNull(RelayTag::parse).mapNotNull { AddressHint(addressTag(), it.url) } + + override fun pubKeyHints() = tags.mapNotNull(ModeratorTag::parseAsHint) + fun name() = tags.firstNotNullOfOrNull(NameTag::parse) fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse) @@ -59,6 +83,8 @@ class CommunityDefinitionEvent( fun relays() = tags.mapNotNull(RelayTag::parse) + fun relayUrls() = tags.mapNotNull(RelayTag::parseUrls) + companion object { const val KIND = 34550 const val ALT_DESCRIPTION = "Community definition" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt index 199446bfac..ba33b8abb0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt @@ -24,14 +24,18 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.ensure @Immutable data class ModeratorTag( override val pubKey: String, - override val relayHint: String?, + override val relayHint: NormalizedRelayUrl?, val role: String?, ) : PubKeyReferenceTag { fun toTagArray() = assemble(pubKey, relayHint, role) @@ -44,7 +48,10 @@ data class ModeratorTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - return ModeratorTag(tag[1], tag.getOrNull(2), tag.getOrNull(3)) + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return ModeratorTag(tag[1], hint, tag.getOrNull(3)) } @JvmStatic @@ -55,11 +62,24 @@ data class ModeratorTag( return tag[1] } + @JvmStatic + fun parseAsHint(tag: Array): PubKeyHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == ParticipantTag.Companion.TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(hint != null) { return null } + + return PubKeyHint(tag[1], hint) + } + @JvmStatic fun assemble( pubkey: HexKey, - relayHint: String?, + relayHint: NormalizedRelayUrl?, role: String?, - ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint, role) + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint?.url, role) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RelayTag.kt index 948f4f35bb..09e62790d6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RelayTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/RelayTag.kt @@ -21,11 +21,13 @@ package com.vitorpamplona.quartz.nip72ModCommunities.definition.tags import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.ensure class RelayTag( - val url: String, + val url: NormalizedRelayUrl, val marker: String? = null, ) { fun toTagArray() = assemble(url, marker) @@ -38,13 +40,27 @@ class RelayTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - return RelayTag(tag[1], tag.getOrNull(2)) + + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null + + return RelayTag(relay, tag.getOrNull(2)) + } + + @JvmStatic + fun parseUrls(tag: Array): NormalizedRelayUrl? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null + + return relay } @JvmStatic fun assemble( - url: String, + relay: NormalizedRelayUrl, marker: String? = null, - ) = arrayOfNotNull(TAG_NAME, url, marker) + ) = arrayOfNotNull(TAG_NAME, relay.url, marker) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt new file mode 100644 index 0000000000..2d5acf30d6 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt @@ -0,0 +1,272 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.follow + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlin.collections.map +import kotlin.collections.plus +import kotlin.coroutines.resume + +@Immutable +class CommunityListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + @Transient var publicAndPrivateAddressCache: Set? = null + + fun publicCommunities() = tags.mapNotNull(ATag::parseAsHint) + + fun publicCommunityIds() = tags.mapNotNull(ATag::parseAddressId) + + fun publicAndPrivateCommunities( + signer: NostrSigner, + onReady: (Set) -> Unit, + ) { + publicAndPrivateAddressCache?.let { eventList -> + onReady(eventList) + return + } + + mergeTagList(signer) { + val set = it.mapNotNull(ATag::parseAsHint).toSet() + publicAndPrivateAddressCache = set + onReady(set) + } + } + + suspend fun publicAndPrivateCommunities(signer: NostrSigner): Set? { + publicAndPrivateAddressCache?.let { return it } + + return tryAndWait { continuation -> + publicAndPrivateCommunities(signer) { privateTagList -> + continuation.resume(privateTagList) + } + } + } + + companion object { + const val KIND = 10004 + const val ALT = "Community List" + const val FIXED_D_TAG = "" + + fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) + + private fun createCommunityBase( + tags: Array>, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + PrivateTagArrayBuilder.create( + tags, + isPrivate, + signer, + ) { encryptedContent, newTags -> + create(encryptedContent, newTags, signer, createdAt, onReady) + } + } + + fun createCommunity( + community: EventHintBundle, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) = createCommunityBase( + tags = arrayOf(ATag.assemble(community.event.address(), community.relay)), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + + fun createCommunity( + community: ATag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) = createCommunityBase( + tags = arrayOf(community.toATagArray()), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + + fun createCommunities( + communities: List>, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) = createCommunityBase( + tags = communities.map { ATag.assemble(it.event.address().toValue(), it.relay) }.toTypedArray(), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + onReady = onReady, + ) + + fun removeCommunity( + earlierVersion: CommunityListEvent, + community: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + PrivateTagArrayBuilder.removeAll( + earlierVersion, + ATag.assemble(community, null), + signer, + ) { encryptedContent, newTags -> + create(encryptedContent, newTags, signer, createdAt, onReady) + } + } + + private fun addCommunityBase( + earlierVersion: CommunityListEvent, + newTags: Array>, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + PrivateTagArrayBuilder.addAll( + earlierVersion, + newTags, + isPrivate, + signer, + ) { encryptedContent, newTags -> + create(encryptedContent, newTags, signer, createdAt, onReady) + } + } + + fun addCommunity( + earlierVersion: CommunityListEvent, + community: ATag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) = addCommunityBase( + earlierVersion, + arrayOf(community.toATagArray()), + isPrivate, + signer, + createdAt, + onReady, + ) + + fun addCommunity( + earlierVersion: CommunityListEvent, + community: EventHintBundle, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) = addCommunityBase( + earlierVersion, + arrayOf(community.toATag().toATagArray()), + isPrivate, + signer, + createdAt, + onReady, + ) + + fun addCommunity( + earlierVersion: CommunityListEvent, + community: AddressHint, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) = addCommunityBase( + earlierVersion, + arrayOf(ATag.assemble(community.addressId, community.relay)), + isPrivate, + signer, + createdAt, + onReady, + ) + + fun addCommunities( + earlierVersion: CommunityListEvent, + communities: List>, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) = addCommunityBase( + earlierVersion, + communities.map { it.toATag().toATagArray() }.toTypedArray(), + isPrivate, + signer, + createdAt, + onReady, + ) + + private fun create( + content: String, + tags: Array>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (CommunityListEvent) -> Unit, + ) { + val newTags = + if (tags.any { it.size > 1 && it[0] == "alt" }) { + tags + } else { + tags + AltTag.assemble(ALT) + } + + signer.sign(createdAt, KIND, newTags, content, onReady) + } + + fun create( + list: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): CommunityListEvent? { + val tags = list.map { ATag.assemble(it.addressId, it.relay) }.toTypedArray() + return signer.sign(createdAt, KIND, tags, "") + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..098657d269 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayBuilderExt.kt @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.follow + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address + +fun TagArrayBuilder.followCommunity( + address: Address, + relayUrl: NormalizedRelayUrl, +) = addUnique(ATag.assemble(address, relayUrl)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt index 9cc32f363c..f55600e480 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt @@ -26,9 +26,18 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.references.reference import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag @@ -46,7 +55,16 @@ class GoalEvent( tags: Array>, content: String, sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + fun topics() = hashtags() fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt index e9eac01f6f..7fbad7d951 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt @@ -22,13 +22,25 @@ package com.vitorpamplona.quartz.nip84Highlights import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.addressables.firstTaggedATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.firstTaggedAddress +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.firstTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.references.ReferenceTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip84Highlights.tags.CommentTag @@ -44,7 +56,16 @@ class HighlightEvent( content: String, sig: HexKey, ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), - RootScope { + RootScope, + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + fun inUrl() = tags.firstNotNullOfOrNull(ReferenceTag::parse) fun author() = firstTaggedUser() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/ClientTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/ClientTag.kt new file mode 100644 index 0000000000..2810fe19ef --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/ClientTag.kt @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip89AppHandlers.clientTag + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip46RemoteSigner.getOrNull +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.ensure + +class ClientTag( + val name: String, + val address: Address?, + val relayHint: NormalizedRelayUrl?, +) { + fun toTagArray() = assemble(name, address, relayHint) + + companion object { + const val TAG_NAME = "client" + + @JvmStatic + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun parse(tag: Array): ClientTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + + val address = tag.getOrNull(2)?.let { Address.parse(it) } + val relayHint = tag.getOrNull(3)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return ClientTag( + tag[1], + address, + relayHint, + ) + } + + @JvmStatic + fun assemble(name: String) = arrayOfNotNull(TAG_NAME, name) + + @JvmStatic + fun assemble( + name: String, + address: String? = null, + relayHint: NormalizedRelayUrl? = null, + ) = arrayOfNotNull(TAG_NAME, name, address, relayHint?.url) + + @JvmStatic + fun assemble( + name: String, + address: Address? = null, + relayHint: NormalizedRelayUrl? = null, + ) = assemble(name, address?.toValue(), relayHint) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/EventExt.kt new file mode 100644 index 0000000000..81b92c0393 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/EventExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip89AppHandlers.clientTag + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount + +fun Event.client() = tags.zapraiserAmount() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SubscriptionCollection.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/TagArrayBuilderExt.kt similarity index 73% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SubscriptionCollection.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/TagArrayBuilderExt.kt index a3d70ff3ef..566492c985 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/SubscriptionCollection.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/TagArrayBuilderExt.kt @@ -18,20 +18,15 @@ * 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.nip01Core.relay +package com.vitorpamplona.quartz.nip89AppHandlers.clientTag import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint -interface SubscriptionCollection { - fun isActive(subscriptionId: String): Boolean +fun TagArrayBuilder.client(name: String) = addUnique(ClientTag.assemble(name)) - fun getFilters(subscriptionId: String): List - - fun allSubscriptions(): List - - fun match( - subscriptionId: String, - event: Event, - ): Boolean -} +fun TagArrayBuilder.client( + name: String, + address: AddressHint, +) = addUnique(ClientTag.assemble(name, address.addressId, address.relay)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/TagArrayExt.kt new file mode 100644 index 0000000000..3516fad880 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/clientTag/TagArrayExt.kt @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip89AppHandlers.clientTag + +import com.vitorpamplona.quartz.nip01Core.core.TagArray + +fun TagArray.client() = this.mapNotNull(ClientTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt index d82fe05224..c8266e82a5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt @@ -24,6 +24,8 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip31Alts.alt @@ -40,7 +42,10 @@ class AppRecommendationEvent( tags: Array>, content: String, sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + AddressHintProvider { + override fun addressHints() = tags.mapNotNull(RecommendationTag::parseAsHint) + fun recommendations() = tags.mapNotNull(RecommendationTag::parse) fun recommendationAddresses() = tags.mapNotNull(RecommendationTag::parseAddressId) @@ -51,7 +56,7 @@ class AppRecommendationEvent( class AppRecommendationItem( val appDefinitionEvent: AppDefinitionEvent, - val relayHint: String?, + val relayHint: NormalizedRelayUrl?, val platform: PlatformType, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/TagArrayBuilderExt.kt index 68f7318f54..96122fd363 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/TagArrayBuilderExt.kt @@ -21,18 +21,19 @@ package com.vitorpamplona.quartz.nip89AppHandlers.recommendation import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.tags.RecommendationTag fun TagArrayBuilder.recommend( addressId: String, - relay: String?, + relay: NormalizedRelayUrl?, platform: String?, ) = add(RecommendationTag.assemble(addressId, relay, platform)) fun TagArrayBuilder.recommend( address: Address, - relay: String?, + relay: NormalizedRelayUrl?, platform: String?, ) = add(RecommendationTag.assemble(address, relay, platform)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/tags/RecommendationTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/tags/RecommendationTag.kt index 1a48b20fad..4056e736eb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/tags/RecommendationTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/tags/RecommendationTag.kt @@ -23,6 +23,10 @@ package com.vitorpamplona.quartz.nip89AppHandlers.recommendation.tags import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.ensure @@ -30,7 +34,7 @@ import com.vitorpamplona.quartz.utils.ensure @Immutable class RecommendationTag( val address: Address, - val relay: String? = null, + val relay: NormalizedRelayUrl? = null, val platform: String? = null, ) { fun toTagArray() = assemble(address, relay, platform) @@ -47,7 +51,8 @@ class RecommendationTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } val address = Address.parse(tag[1]) ?: return null - return RecommendationTag(address, tag.getOrNull(2), tag.getOrNull(3)) + val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + return RecommendationTag(address, relayHint, tag.getOrNull(3)) } @JvmStatic @@ -58,18 +63,31 @@ class RecommendationTag( return tag[1] } + @JvmStatic + fun parseAsHint(tag: Array): AddressHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == ATag.Companion.TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + ensure(tag[1].contains(':')) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) ?: return null + + return AddressHint(tag[1], relayHint) + } + @JvmStatic fun assemble( addressId: String, - relay: String?, + relay: NormalizedRelayUrl?, platform: String?, - ) = arrayOfNotNull(TAG_NAME, addressId, relay, platform) + ) = arrayOfNotNull(TAG_NAME, addressId, relay?.url, platform) @JvmStatic fun assemble( address: Address, - relay: String?, + relay: NormalizedRelayUrl?, platform: String?, - ) = arrayOfNotNull(TAG_NAME, address.toValue(), relay, platform) + ) = arrayOfNotNull(TAG_NAME, address.toValue(), relay?.url, platform) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt index c2c44c5b85..c31dd4a47e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -45,7 +46,7 @@ class NIP90ContentDiscoveryRequestEvent( fun create( dvmPublicKey: HexKey, forUser: HexKey, - relays: Set, + relays: Set, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (NIP90ContentDiscoveryRequestEvent) -> Unit, @@ -54,7 +55,7 @@ class NIP90ContentDiscoveryRequestEvent( val tags = mutableListOf>() tags.add(arrayOf("p", dvmPublicKey)) tags.add(AltTag.assemble(ALT)) - tags.add(arrayOf("relays") + relays.toTypedArray()) + tags.add(arrayOf("relays") + relays.map { it.url }.toTypedArray()) tags.add(arrayOf("param", "max_results", "200")) tags.add(arrayOf("param", "user", forUser)) signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomAuthorizationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomAuthorizationEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt index 4fa1ad9ba1..d2db684831 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomAuthorizationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.blossom +package com.vitorpamplona.quartz.nipB7Blossom import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt index 71df0ede5a..817ed20d29 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/blossom/BlossomServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.blossom +package com.vitorpamplona.quartz.nipB7Blossom import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt index fd0a4bff48..65d5cff60d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt @@ -26,6 +26,8 @@ import java.util.function.BiConsumer class LargeCache { private val cache = ConcurrentSkipListMap() + fun keys() = cache.keys + fun values() = cache.values fun get(key: K) = cache.get(key) @@ -36,6 +38,8 @@ class LargeCache { fun isEmpty() = cache.isEmpty() + fun clear() = cache.clear() + fun containsKey(key: K) = cache.containsKey(key) fun put( @@ -59,6 +63,19 @@ class LargeCache { } } + fun createIfAbsent( + key: K, + builder: (key: K) -> V, + ): Boolean { + val value = cache.get(key) + return if (value != null) { + false + } else { + val newObject = builder(key) + cache.putIfAbsent(key, newObject) == null + } + } + fun forEach(consumer: BiConsumer) { innerForEach(consumer) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt new file mode 100644 index 0000000000..698daf72bf --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils + +import android.R.attr.value + +class MapOfSetBuilder { + val data = mutableMapOf>() + + fun add( + key: K, + value: V, + ) { + val set = data[key] + if (set == null) { + data.put(key, mutableSetOf(value)) + } else { + set.add(value) + } + } + + fun add( + key: K, + value: Set, + ) { + val set = data[key] + if (set == null) { + data.put(key, value.toMutableSet()) + } else { + set.addAll(value) + } + } + + fun add(map: Map>) { + map.forEach { + add(it.key, it.value) + } + } + + fun build(): Map> = data +} + +fun mapOfSet(init: MapOfSetBuilder.() -> Unit): Map> { + val data = MapOfSetBuilder() + data.init() + return data.build() +} + +fun merge(maps: List>>): Map> { + return mapOfSet { + maps.forEach { map -> + add(map) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ParallelUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ParallelUtils.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt index 7b48e4c071..bf46ca8af7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ParallelUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst +package com.vitorpamplona.quartz.utils import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt index 84b90b82ca..29458b572c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.utils +import com.vitorpamplona.quartz.utils.RandomInstance.charPool import java.security.SecureRandom object RandomInstance { @@ -28,4 +29,10 @@ object RandomInstance { fun int(bound: Int = Int.MAX_VALUE) = randomizer.nextInt(bound) fun bytes(size: Int) = ByteArray(size).also { randomizer.nextBytes(it) } + + val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + + fun randomChar() = charPool[randomizer.nextInt(charPool.size)] + + fun randomChars(size: Int = 16) = String(CharArray(size) { randomChar() }) } diff --git a/quartz/src/test/java/android/util/Log.java b/quartz/src/test/java/android/util/Log.java new file mode 100644 index 0000000000..af85d0b47e --- /dev/null +++ b/quartz/src/test/java/android/util/Log.java @@ -0,0 +1,35 @@ +package android.util; + +public class Log { + public static int d(String tag, String msg) { + System.out.println("DEBUG: " + tag + ": " + msg); + return 0; + } + + public static int i(String tag, String msg) { + System.out.println("INFO: " + tag + ": " + msg); + return 0; + } + + public static int w(String tag, String msg) { + System.out.println("WARN: " + tag + ": " + msg); + return 0; + } + + public static int w(String tag, String msg, Throwable e) { + System.out.println("WARN: " + tag + ": " + msg); + e.printStackTrace(); + return 0; + } + + public static int e(String tag, String msg) { + System.out.println("ERROR: " + tag + ": " + msg); + return 0; + } + + public static int e(String tag, String msg, Throwable e) { + System.out.println("ERROR: " + tag + ": " + msg); + e.printStackTrace(); + return 0; + } +} \ No newline at end of file diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt index 341e4074c0..4a265ad020 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.hints import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.usedMemoryMb @@ -62,7 +63,7 @@ class HintIndexerTest { .getResourceAsStream("relayDB.txt") ?.readAllBytes() .toString() - .split("\n") + .split("\n").mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) } val indexer by lazy { System.gc() @@ -119,31 +120,31 @@ class HintIndexerTest { @Test fun runProbExistingKeys() = assert99PercentSucess { - indexer.getKey(keys.random()).isNotEmpty() + indexer.hintsForKey(keys.random()).isNotEmpty() } @Test fun runProbNewKeys() = assert99PercentSucess { - indexer.getKey(RandomInstance.bytes(32)).isEmpty() + indexer.hintsForKey(RandomInstance.bytes(32)).isEmpty() } @Test fun runProbExistingEventIds() = assert99PercentSucess { - indexer.getEvent(eventIds.random()).isNotEmpty() + indexer.hintsForEvent(eventIds.random()).isNotEmpty() } @Test fun runProbNewEventIds() = assert99PercentSucess { - indexer.getEvent(RandomInstance.bytes(32)).isEmpty() + indexer.hintsForEvent(RandomInstance.bytes(32)).isEmpty() } @Test fun runProbExistingAddresses() = assert99PercentSucess { - indexer.getAddress(addresses.random()).isNotEmpty() + indexer.hintsForAddress(addresses.random()).isNotEmpty() } @Test @@ -156,6 +157,6 @@ class HintIndexerTest { randomChars(10), ) - indexer.getAddress(newAddress).isEmpty() + indexer.hintsForAddress(newAddress).isEmpty() } } diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/relay/RelayUrlFormatterTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/relay/RelayUrlFormatterTest.kt new file mode 100644 index 0000000000..fd936867ac --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/relay/RelayUrlFormatterTest.kt @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay + +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import junit.framework.TestCase.assertEquals +import org.junit.Test + +class RelayUrlFormatterTest { + @Test + fun format() { + assertEquals("wss://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("wss://nostr.mom")?.url) + assertEquals("wss://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("nostr.mom")?.url) + assertEquals("ws://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("ws://nostr.mom")?.url) + assertEquals("wss://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("wss://nostr.mom/")?.url) + assertEquals("wss://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("https://nostr.mom/")?.url) + assertEquals("ws://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("http://nostr.mom/")?.url) + + assertEquals("wss://localhost:3030/", RelayUrlNormalizer.normalizeOrNull("wss://localhost:3030")?.url) + assertEquals("ws://localhost:3030/", RelayUrlNormalizer.normalizeOrNull("localhost:3030")?.url) + + assertEquals("wss://a.onion/", RelayUrlNormalizer.normalizeOrNull("wss://a.onion")?.url) + assertEquals("ws://a.onion/", RelayUrlNormalizer.normalizeOrNull("a.onion")?.url) + assertEquals("wss://a.onion/", RelayUrlNormalizer.normalizeOrNull("wss://a.onion/")?.url) + assertEquals("ws://a.onion/", RelayUrlNormalizer.normalizeOrNull("a.onion/")?.url) + + assertEquals("wss://nostr.mom/", RelayUrlNormalizer.normalizeOrNull("wss://nostr.mom")?.url) + } +} diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt index 12b91c6360..f1143e1ce3 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.quartz.nip19Bech32 +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress @@ -107,7 +109,7 @@ class NIP19ParserTest { Assert.assertNotNull(actual) Assert.assertTrue(actual?.entity is NProfile) Assert.assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", (actual?.entity as? NProfile)?.hex) - Assert.assertEquals("wss://vitor.nostr1.com/", (actual?.entity as? NProfile)?.relay?.first()) + Assert.assertEquals(NormalizedRelayUrl("wss://vitor.nostr1.com/"), (actual?.entity as? NProfile)?.relay?.first()) } @Test() @@ -169,7 +171,7 @@ class NIP19ParserTest { "30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", (result?.entity as? NAddress)?.aTag(), ) - assertEquals("wss://relay.damus.io", (result?.entity as? NAddress)?.relay?.get(0)) + assertEquals(NormalizedRelayUrl("wss://relay.damus.io/"), (result?.entity as? NAddress)?.relay?.get(0)) } @Test @@ -177,7 +179,7 @@ class NIP19ParserTest { val address = ATag.parse( "30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", - "wss://relay.damus.io", + "relay.damus.io", ) assertEquals(30023, address?.kind) assertEquals( @@ -185,9 +187,9 @@ class NIP19ParserTest { address?.pubKeyHex, ) assertEquals("89de7920", address?.dTag) - assertEquals("wss://relay.damus.io", address?.relay) + assertEquals("wss://relay.damus.io/", address?.relay?.url) assertEquals( - "naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", + "naddr1qqyrswtyv5mnjv3sqy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7q3q68nqgewzkamnyh53x0epqrftkv2pdh9gzr6558v4vetzr3w7uxfsxpqqqp65wmpxfdu", address?.toNAddr(), ) } @@ -229,10 +231,10 @@ class NIP19ParserTest { 30023, "d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", "89de7920", - "wss://relay.damus.io", + RelayUrlNormalizer.normalizeOrNull("wss://relay.damus.io")!!, ) assertEquals( - "naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", + "naddr1qqyrswtyv5mnjv3sqy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7q3q68nqgewzkamnyh53x0epqrftkv2pdh9gzr6558v4vetzr3w7uxfsxpqqqp65wmpxfdu", address.toNAddr(), ) } @@ -298,7 +300,7 @@ class NIP19ParserTest { assertNotNull(result) assertEquals("d768c1b28eb94c7d90aa6b9152021fb69c8e34b452f870e2e42341ea7f9796ca", result?.hex) - assertEquals("wss://relay.nostr.bg/", result?.relay?.firstOrNull()) + assertEquals("wss://relay.nostr.bg/", result?.relay?.firstOrNull()?.url) assertEquals("cac0e43235806da094f0787a5b04e29ad04cb1a3c7ea5cf61edc1c338734082b", result?.author) assertEquals(1, result?.kind) } @@ -322,7 +324,7 @@ class NIP19ParserTest { assertNotNull(result) assertEquals("9677aa74676757cafad5910f46a1a35f58f7bae253b03eb8ffa70db2fb4643ea", result?.hex) - assertEquals("wss://relay.westernbtc.com", result?.relay?.firstOrNull()) + assertEquals("wss://relay.westernbtc.com/", result?.relay?.firstOrNull()?.url) assertEquals(null, result?.author) assertEquals(null, result?.kind) } @@ -337,7 +339,10 @@ class NIP19ParserTest { assertNotNull(result) assertEquals("b60ffa7256d3dd7543d830eb717ae50d05a6c32c5f791ed15b867c2bb0b954ac", result?.hex) - assertEquals("wss://nostr.mom", result?.relay?.get(0)) + assertEquals( + NormalizedRelayUrl("wss://nostr.mom/"), + result?.relay?.get(0), + ) assertEquals("f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a", result?.author) assertEquals(1, result?.kind) } @@ -352,7 +357,7 @@ class NIP19ParserTest { assertNotNull(result) assertEquals("1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0", result?.hex) - assertEquals("wss://relay.damus.io", result?.relay?.get(0)) + assertEquals(NormalizedRelayUrl("wss://relay.damus.io/"), result?.relay?.get(0)) assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author) assertEquals(1, result?.kind) } @@ -362,14 +367,14 @@ class NIP19ParserTest { val result = Nip19Parser .uriToRoute( - "nostr:nevent1qqsg6gechd3dhzx38n4z8a2lylzgsmmgeamhmtzz72m9ummsnf0xjfspsdmhxue69uhkummn9ekx7mpvwaehxw309ahx7um5wghx77r5wghxgetk93mhxue69uhhyetvv9ujumn0wd68ytnzvuk8wumn8ghj7mn0wd68ytn9d9h82mny0fmkzmn6d9njuumsv93k2trhwden5te0wfjkccte9ehx7um5wghxyctwvsk8wumn8ghj7un9d3shjtnyv9kh2uewd9hs3kqsdn", + "nostr:nevent1qqsg6gechd3dhzx38n4z8a2lylzgsmmgeamhmtzz72m9ummsnf0xjfsppemhxue69uhkummn9ekx7mp0qy2hwumn8ghj7mn0wd68ytn00p68ytnyv4mz7qg4waehxw309aex2mrp0yhxummnw3ezucn89uqjqamnwvaz7tmwdaehgu3wv45kuatwv3a8wctw0f5kwtnnwpskxef0qythwumn8ghj7un9d3shjtnwdaehgu3wvfskuep0qy2hwumn8ghj7un9d3shjtnyv9kh2uewd9hj7dyp4wy", )?.entity as? NEvent assertNotNull(result) assertEquals("8d2338bb62db88d13cea23f55f27c4886f68cf777dac42f2b65e6f709a5e6926", result?.hex) assertEquals( - "wss://nos.lol,wss://nostr.oxtr.dev,wss://relay.nostr.bg,wss://nostr.einundzwanzig.space,wss://relay.nostr.band,wss://relay.damus.io", - result?.relay?.joinToString(","), + "wss://nos.lol/,wss://nostr.oxtr.dev/,wss://relay.nostr.bg/,wss://nostr.einundzwanzig.space/,wss://relay.nostr.band/,wss://relay.damus.io/", + result?.relay?.joinToString(",") { it.url }, ) } @@ -383,7 +388,7 @@ class NIP19ParserTest { assertNotNull(result) assertEquals("4300ec7fa2f98a276f033908349651620aa8e282b76030ab22abca63e85e07e6", result?.hex) - assertEquals("wss://relay.damus.io", result?.relay?.get(0)) + assertEquals("wss://relay.damus.io/", result?.relay?.get(0)?.url) assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author) assertEquals(1, result?.kind) } @@ -422,10 +427,10 @@ class NIP19ParserTest { "1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", 1, - "wss://relay.damus.io", + RelayUrlNormalizer.normalizeOrNull("wss://relay.damus.io"), ) assertEquals( - "nevent1qqsplpuwsgrrmq85rfup6w3w777rxmcmadu590emfx6z4msj2844euqpz3mhxue69uhhyetvv9ujuerpd46hxtnfdupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqvzqqqqqqye3a70w", + "nevent1qqsplpuwsgrrmq85rfup6w3w777rxmcmadu590emfx6z4msj2844euqpz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqqqqsqvc0ku", nevent, ) } diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessorTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessorTest.kt index 1b40d70fa0..b90ade3333 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessorTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessorTest.kt @@ -20,29 +20,32 @@ */ package com.vitorpamplona.quartz.nip65RelayList +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue import org.junit.Test class RelayListRecommendationProcessorTest { + fun norm(str: String) = RelayUrlNormalizer.normalizeOrNull(str)!! + val userList = mutableMapOf( - "User1" to mutableSetOf("wss://relay1.com", "wss://relay2.com", "wss://relay3.com"), - "User2" to mutableSetOf("wss://relay4.com", "wss://relay5.com", "wss://relay6.com"), - "User3" to mutableSetOf("wss://relay1.com", "wss://relay4.com", "wss://relay6.com"), - "User4" to mutableSetOf("wss://relay2.com", "wss://relay1.com", "wss://relay4.com"), + "User1" to mutableSetOf(norm("wss://relay1.com"), norm("wss://relay2.com"), norm("wss://relay3.com")), + "User2" to mutableSetOf(norm("wss://relay4.com"), norm("wss://relay5.com"), norm("wss://relay6.com")), + "User3" to mutableSetOf(norm("wss://relay1.com"), norm("wss://relay4.com"), norm("wss://relay6.com")), + "User4" to mutableSetOf(norm("wss://relay2.com"), norm("wss://relay1.com"), norm("wss://relay4.com")), ) @Test fun testTranspose() { assertEquals( mapOf( - "wss://relay1.com" to listOf("User1", "User3", "User4"), - "wss://relay2.com" to listOf("User1", "User4"), - "wss://relay3.com" to listOf("User1"), - "wss://relay4.com" to listOf("User2", "User3", "User4"), - "wss://relay5.com" to listOf("User2"), - "wss://relay6.com" to listOf("User2", "User3"), + norm("wss://relay1.com") to listOf("User1", "User3", "User4"), + norm("wss://relay2.com") to listOf("User1", "User4"), + norm("wss://relay3.com") to listOf("User1"), + norm("wss://relay4.com") to listOf("User2", "User3", "User4"), + norm("wss://relay5.com") to listOf("User2"), + norm("wss://relay6.com") to listOf("User2", "User3"), ).toString(), RelayListRecommendationProcessor.transpose(userList).toString(), ) @@ -54,7 +57,7 @@ class RelayListRecommendationProcessorTest { val rec1 = recommendations[0] - assertEquals("wss://relay1.com", rec1.url) + assertEquals("wss://relay1.com/", rec1.relay.url) assertEquals(true, rec1.requiredToNotMissEvents) assertTrue("User1" in rec1.users) assertTrue("User2" !in rec1.users) @@ -63,7 +66,7 @@ class RelayListRecommendationProcessorTest { val rec2 = recommendations[1] - assertEquals("wss://relay4.com", rec2.url) + assertEquals("wss://relay4.com/", rec2.relay.url) assertEquals(true, rec2.requiredToNotMissEvents) assertTrue("User1" !in rec2.users) assertTrue("User2" in rec2.users) @@ -72,7 +75,7 @@ class RelayListRecommendationProcessorTest { val rec3 = recommendations[2] - assertEquals("wss://relay2.com", rec3.url) + assertEquals("wss://relay2.com/", rec3.relay.url) assertEquals(false, rec3.requiredToNotMissEvents) assertTrue("User1" in rec3.users) assertTrue("User2" !in rec3.users) @@ -81,7 +84,7 @@ class RelayListRecommendationProcessorTest { val rec4 = recommendations[3] - assertEquals("wss://relay5.com", rec4.url) + assertEquals("wss://relay5.com/", rec4.relay.url) assertEquals(false, rec4.requiredToNotMissEvents) assertTrue("User1" !in rec4.users) assertTrue("User2" in rec4.users) From 1615337a2c97c0eee174387e4a34149156d2c13a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 08:58:16 -0400 Subject: [PATCH 002/185] Simplifies new try --- .../nip01Core/relay/client/pool/PoolEventOutboxRepository.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxRepository.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxRepository.kt index b85419258c..8ca1e03c90 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxRepository.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutboxRepository.kt @@ -57,10 +57,7 @@ class PoolEventOutboxRepository { id: HexKey, url: NormalizedRelayUrl, ) { - val waiting = eventOutbox[id] - if (waiting != null) { - waiting.newTry(url) - } + eventOutbox[id]?.newTry(url) } fun newResponse( From b236938ef5350145a64ae9ec51a677cd544f989c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 08:59:03 -0400 Subject: [PATCH 003/185] Simplify the unregistering instead of relying on errors and other states that might come from separate filters to the same relay --- .../NostrClientSingleDownloadExt.kt | 56 ++++--------------- 1 file changed, 12 insertions(+), 44 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt index cd2645eed0..29fff89413 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt @@ -23,17 +23,20 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.acessories import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener -import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch fun NostrClient.downloadFirstEvent( subscriptionId: String = newSubId(), filters: List = listOf(), onResponse: (Event) -> Unit, ) { - subscribe( + val listener = object : IRelayClientListener { override fun onEvent( relay: IRelayClient, @@ -49,49 +52,14 @@ fun NostrClient.downloadFirstEvent( onResponse(event) } } + } - override fun onClosed( - relay: IRelayClient, - subId: String, - message: String, - ) { - unsubscribe(this) - close(subscriptionId) - super.onClosed(relay, subId, message) - } - - override fun onEOSE( - relay: IRelayClient, - subId: String, - arrivalTime: Long, - ) { - unsubscribe(this) - close(subscriptionId) - super.onEOSE(relay, subId, arrivalTime) - } - - override fun onError( - relay: IRelayClient, - subId: String, - error: Error, - ) { - unsubscribe(this) - close(subscriptionId) - super.onError(relay, subId, error) - } - - override fun onRelayStateChange( - relay: IRelayClient, - type: RelayState, - ) { - if (type == RelayState.DISCONNECTED) { - unsubscribe(this) - close(subscriptionId) - } - super.onRelayStateChange(relay, type) - } - }, - ) + subscribe(listener) sendFilter(subscriptionId, filters) + + GlobalScope.launch(Dispatchers.IO) { + delay(30000) + unsubscribe(listener) + } } From 1c4b2e9d1a20ac7e3717d57ef7cc2f4cf9e39934 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 08:59:12 -0400 Subject: [PATCH 004/185] Recover Send and Wait --- .../acessories/NostrClientSendAndWait.kt | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSendAndWait.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSendAndWait.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSendAndWait.kt new file mode 100644 index 0000000000..351041d9eb --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSendAndWait.kt @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.acessories + +import android.util.Log +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.DelicateCoroutinesApi +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +@OptIn(DelicateCoroutinesApi::class) +suspend fun NostrClient.sendAndWaitForResponse( + event: Event, + relayList: Set, + timeoutInSeconds: Long = 15, +): Boolean { + val size = relayList.size + val latch = CountDownLatch(size) + val relayResults = mutableMapOf() + var result = false + + Log.d("sendAndWaitForResponse", "Waiting for $size responses") + + val subscription = + object : IRelayClientListener { + override fun onError( + relay: IRelayClient, + subId: String, + error: Error, + ) { + if (relay.url in relayList && relayResults[relay.url] == null) { + relayResults[relay.url] = false + latch.countDown() + } + Log.d("sendAndWaitForResponse", "onError Error from relay ${relay.url} count: ${latch.count} error: $error") + } + + override fun onRelayStateChange( + relay: IRelayClient, + type: RelayState, + ) { + if (type == RelayState.DISCONNECTED) { + if (relay.url in relayList && relayResults[relay.url] == null) { + relayResults[relay.url] = false + latch.countDown() + } + } + Log.d("sendAndWaitForResponse", "onRelayStateChange ${type.name} from relay ${relay.url} count: ${latch.count}") + } + + override fun onSendResponse( + relay: IRelayClient, + eventId: String, + success: Boolean, + message: String, + ) { + if (eventId == event.id) { + if (relayResults[relay.url] == null) { + latch.countDown() + relayResults[relay.url] = success + } else { + if (success && relayResults[relay.url] == false) { + relayResults[relay.url] = true + } + } + + if (success) { + result = true + } + + Log.d("sendAndWaitForResponse", "onSendResponse Received response for $eventId from relay ${relay.url} count: ${latch.count} message $message success $success") + } + } + } + + subscribe(subscription) + + send(event, relayList) + + latch.await(timeoutInSeconds, TimeUnit.SECONDS) + + unsubscribe(subscription) + + Log.d("sendAndWaitForResponse", "countdown finished") + + return result +} From 774ee20a2060c7533578af902de0b1f9635012c3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 10:51:03 -0400 Subject: [PATCH 005/185] Updates all dependencies --- .../notifications/PushDistributorHandler.kt | 6 +-- .../notifications/PushMessageReceiver.kt | 12 +++-- .../service/playback/pip/PipVideoActivity.kt | 5 -- .../ui/components/MediaCompressorTest.kt | 8 +-- gradle/libs.versions.toml | 50 +++++++++---------- 5 files changed, 40 insertions(+), 41 deletions(-) diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt index 2bb1fdda0f..8f832704ec 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushDistributorHandler.kt @@ -89,14 +89,14 @@ object PushDistributorHandler : PushDistributorActions { override fun saveDistributor(distributor: String) { unifiedPush.saveDistributor(appContext(), distributor) - unifiedPush.registerApp(appContext()) + unifiedPush.register(appContext()) } override fun removeSavedDistributor() { - unifiedPush.safeRemoveDistributor(appContext()) + unifiedPush.removeDistributor(appContext()) } fun forceRemoveDistributor(context: Context) { - unifiedPush.forceRemoveDistributor(context) + UnifiedPush.removeDistributor(context) } } diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt index 33b3ffdc76..eed9a8c81b 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/service/notifications/PushMessageReceiver.kt @@ -34,7 +34,10 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.unifiedpush.android.connector.FailedReason import org.unifiedpush.android.connector.MessagingReceiver +import org.unifiedpush.android.connector.data.PushEndpoint +import org.unifiedpush.android.connector.data.PushMessage class PushMessageReceiver : MessagingReceiver() { companion object { @@ -48,10 +51,10 @@ class PushMessageReceiver : MessagingReceiver() { override fun onMessage( context: Context, - message: ByteArray, + message: PushMessage, instance: String, ) { - val messageStr = message.decodeToString() + val messageStr = message.content.decodeToString() Log.d(TAG, "New message $messageStr for Instance: $instance") scope.launch { try { @@ -81,10 +84,10 @@ class PushMessageReceiver : MessagingReceiver() { override fun onNewEndpoint( context: Context, - endpoint: String, + endpoint: PushEndpoint, instance: String, ) { - val sanitizedEndpoint = if (endpoint.endsWith("?up=1")) endpoint.dropLast(5) else endpoint + val sanitizedEndpoint = if (endpoint.url.endsWith("?up=1")) endpoint.url.dropLast(5) else endpoint.url if (sanitizedEndpoint != pushHandler.getSavedEndpoint()) { Log.d(TAG, "New endpoint provided:- $endpoint for Instance: $instance ${pushHandler.getSavedEndpoint()} $sanitizedEndpoint") pushHandler.setEndpoint(sanitizedEndpoint) @@ -102,6 +105,7 @@ class PushMessageReceiver : MessagingReceiver() { override fun onRegistrationFailed( context: Context, + reason: FailedReason, instance: String, ) { Log.d(TAG, "Registration failed for Instance: $instance") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoActivity.kt index 8a1ff7d231..3cde5e550c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoActivity.kt @@ -67,11 +67,6 @@ class PipVideoActivity : ComponentActivity() { super.finish() } - override fun onBackPressed() { - super.onBackPressed() - finishAndRemoveTask() - } - override fun onUserLeaveHint() { super.onUserLeaveHint() finishAndRemoveTask() diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt index 3716170c4e..5b07e82f2a 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/components/MediaCompressorTest.kt @@ -81,7 +81,7 @@ class MediaCompressorTest { ) // Verify - verify(exactly = 0) { VideoCompressor.start(any(), any(), any(), any(), any(), any(), any()) } + verify(exactly = 0) { VideoCompressor.start(any(), any(), any(), any(), any(), any()) } coVerify(exactly = 0) { Compressor.compress(any(), any(), any(), any()) } } @@ -102,7 +102,7 @@ class MediaCompressorTest { ) // Verify - verify(exactly = 0) { VideoCompressor.start(any(), any(), any(), any(), any(), any(), any()) } + verify(exactly = 0) { VideoCompressor.start(any(), any(), any(), any(), any(), any()) } coVerify(exactly = 0) { Compressor.compress(any(), any(), any(), any()) } } @@ -115,7 +115,7 @@ class MediaCompressorTest { val uri = mockk() val contentType = "video" - every { VideoCompressor.start(any(), any(), any(), any(), any(), any(), any()) } returns Unit + every { VideoCompressor.start(any(), any(), any(), any(), any(), any()) } returns Unit // Execution MediaCompressor().compress( @@ -126,7 +126,7 @@ class MediaCompressorTest { ) // Verify - verify(exactly = 1) { VideoCompressor.start(any(), any(), any(), any(), any(), any(), any()) } + verify(exactly = 1) { VideoCompressor.start(any(), any(), any(), any(), any(), any()) } } @Test diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac89c22fbe..af738062c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,56 +2,56 @@ accompanistAdaptive = "0.37.3" activityCompose = "1.10.1" agp = "8.11.0" -android-compileSdk = "35" +android-compileSdk = "36" android-minSdk = "26" -android-targetSdk = "35" +android-targetSdk = "36" androidKotlinGeohash = "b481c6a64e" androidxJunit = "1.2.1" -appcompat = "1.7.0" -audiowaveform = "1.1.1" +appcompat = "1.7.1" +audiowaveform = "1.1.2" benchmark = "1.3.4" benchmarkJunit4 = "1.3.4" biometricKtx = "1.2.0-alpha05" coil = "3.2.0" -composeBom = "2025.05.00" +composeBom = "2025.06.01" coreKtx = "1.16.0" espressoCore = "3.6.1" -firebaseBom = "33.13.0" -fragmentKtx = "1.8.6" -gms = "4.4.2" -jacksonModuleKotlin = "2.19.0" +firebaseBom = "33.16.0" +fragmentKtx = "1.8.8" +gms = "4.4.3" +jacksonModuleKotlin = "2.19.1" jna = "5.17.0" jtorctl = "0.4.5.7" junit = "4.13.2" -kotlin = "2.1.0" +kotlin = "2.2.0" kotlinxCollectionsImmutable = "0.4.0" -kotlinxSerialization = "1.8.1" -kotlinxSerializationPlugin = "2.0.0" +kotlinxSerialization = "1.9.0" +kotlinxSerializationPlugin = "2.2.0" languageId = "17.0.6" -lazysodiumAndroid = "5.1.0" -lifecycleRuntimeKtx = "2.9.0" -lightcompressor = "1.3.2" +lazysodiumAndroid = "5.2.0" +lifecycleRuntimeKtx = "2.9.1" +lightcompressor = "1.3.3" markdown = "e1151c8" media3 = "1.7.1" -mockk = "1.14.2" +mockk = "1.14.4" kotlinx-coroutines-test = "1.10.2" navigationCompose = "2.9.0" -okhttp = "5.0.0-alpha.14" +okhttp = "5.0.0-alpha.17" runner = "1.6.2" rfc3986 = "0.1.2" -secp256k1KmpJniAndroid = "0.17.3" -securityCryptoKtx = "1.1.0-alpha07" -spotless = "6.25.0" -torAndroid = "0.4.8.12" +secp256k1KmpJniAndroid = "0.18.0" +securityCryptoKtx = "1.1.0-beta01" +spotless = "7.0.4" +torAndroid = "0.4.8.16.2" translate = "17.0.3" -unifiedpush = "2.3.1" +unifiedpush = "3.0.10" urlDetector = "0.1.23" -vico-charts = "2.1.2" +vico-charts = "2.1.3" zelory = "3.0.1" -zoomable = "2.7.0" +zoomable = "2.8.1" zxing = "3.5.3" zxingAndroidEmbedded = "4.3.0" -windowCoreAndroid = "1.3.0" +windowCoreAndroid = "1.4.0" androidxCamera = "1.4.2" [libraries] From 546d238db3bfd750a55cdea9935f5ace81335ccc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 10:51:13 -0400 Subject: [PATCH 006/185] Reformats due to new lint --- .../vitorpamplona/amethyst/model/Account.kt | 81 +++++++++++++++---- .../amethyst/model/AccountSettings.kt | 5 +- .../amethyst/model/AccountSyncedSettings.kt | 4 +- .../model/emphChat/EphemeralChatListState.kt | 10 +-- .../model/localRelays/LocalRelayListState.kt | 4 +- .../FollowListOutboxRelays.kt | 38 +++++---- .../nip02FollowLists/FollowsPerOutboxRelay.kt | 29 +++---- .../nip28PublicChats/PublicChatListState.kt | 10 +-- .../model/nip51Lists/BlockPeopleListState.kt | 10 +-- .../model/nip51Lists/GeohashListState.kt | 10 +-- .../model/nip51Lists/HashtagListState.kt | 10 +-- .../model/nip51Lists/HiddenUsersState.kt | 5 +- .../model/nip51Lists/MuteListState.kt | 10 +-- .../nip65RelayList/OutboxRelaySetState.kt | 46 ++++++----- .../nip72Communities/CommunityListState.kt | 10 +-- .../serverList/MergedFollowListsState.kt | 27 +++---- .../MergedFollowPlusMineRelayListsState.kt | 28 +++---- .../model/serverList/MergedServerListState.kt | 3 +- .../serverList/TrustedRelayListsState.kt | 28 +++---- .../model/topNavFeeds/CommunityRelayLoader.kt | 26 ++++-- .../topNavFeeds/FeedTopNavFilterState.kt | 11 ++- .../MergedTopFeedAuthorListsState.kt | 13 ++- .../model/topNavFeeds/OutboxLoaderState.kt | 11 +-- .../model/topNavFeeds/OutboxRelayLoader.kt | 19 +++-- .../AllFollowsByOutboxTopNavFilter.kt | 9 +-- .../allFollows/AllFollowsFeedFlow.kt | 5 +- .../topNavFeeds/aroundMe/AroundMeFeedFlow.kt | 5 +- .../aroundMe/LocationTopNavFilter.kt | 9 +-- .../topNavFeeds/global/GlobalTopNavFilter.kt | 10 +-- .../hashtag/HashtagTopNavFilter.kt | 15 ++-- .../AllCommunitiesTopNavFilter.kt | 14 ++-- .../author/AuthorsByOutboxTopNavFilter.kt | 13 +-- .../community/SingleCommunityTopNavFilter.kt | 5 +- .../muted/MutedAuthorsByOutboxTopNavFilter.kt | 13 +-- .../unknown/UnknownTopNavFilter.kt | 4 +- .../model/torState/TorRelayEvaluation.kt | 5 +- .../amethyst/model/torState/TorRelayState.kt | 13 ++- .../amethyst/service/ZapPaymentHandler.kt | 9 +-- .../service/okhttp/ProxySettingsAnchor.kt | 2 +- .../relayClient/RelayProxyClientConnector.kt | 21 ++--- .../AccountGiftWrapsEoseManager.kt | 5 +- .../LiveActivityWatcherSubAssembly.kt | 5 +- .../watchers/EventWatcherSubAssembler.kt | 6 +- .../user/loaders/FilterUserMetadataForKey.kt | 5 +- .../user/watchers/FilterUserMetadataForKey.kt | 16 ++-- .../service/uploads/MediaCompressor.kt | 2 +- .../amethyst/ui/actions/EditPostView.kt | 6 +- .../amethyst/ui/actions/NewMediaView.kt | 9 ++- .../NormalizedRelayUrlSerializer.kt | 4 +- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 3 +- .../amethyst/ui/note/UserProfilePicture.kt | 3 +- .../creators/uploads/ImageVideoDescription.kt | 3 +- .../ui/note/elements/DisplayHashtags.kt | 3 +- .../nip22Comments/CommentPostViewModel.kt | 24 +++--- .../amethyst/ui/note/types/CommunityHeader.kt | 3 +- .../ui/screen/loggedIn/AccountViewModel.kt | 44 +++++----- .../ui/screen/loggedIn/LoggedInPage.kt | 6 +- .../metadata/NewEphemeralChatMetaViewModel.kt | 3 +- .../FilterLastMessageFollowingPublicChats.kt | 53 ++++++------ .../rooms/datasource/FilterNip04DMsFromMe.kt | 5 +- .../rooms/datasource/FilterNip04DMsToMe.kt | 5 +- .../chats/utils/ChatFileUploadDialog.kt | 3 +- .../datasource/FilterCommunityPosts.kt | 5 +- .../nip23LongForm/SubAssemblyHelper.kt | 5 +- .../FilterLongFormByAllCommunities.kt | 15 ++-- .../subassemblies/FilterLongFormByAuthors.kt | 46 ++++++----- .../FilterLongFormByCommunity.kt | 17 ++-- .../subassemblies/FilterLongFormByGeohash.kt | 23 +++--- .../subassemblies/FilterLongFormByHashtag.kt | 23 +++--- .../discover/nip28Chats/SubAssemblyHelper.kt | 5 +- .../FilterPublicChatsByAllCommunities.kt | 15 ++-- .../FilterPublicChatsByAuthors.kt | 46 ++++++----- .../FilterPublicChatsByCommunity.kt | 17 ++-- .../FilterPublicChatsByGeohash.kt | 23 +++--- .../FilterPublicChatsByHashtag.kt | 23 +++--- .../nip51FollowSets/SubAssemblyHelper.kt | 5 +- .../FilterFollowSetsByAllCommunities.kt | 15 ++-- .../FilterFollowSetsByAuthors.kt | 46 ++++++----- .../FilterFollowSetsByCommunity.kt | 17 ++-- .../FilterFollowSetsByGeohash.kt | 23 +++--- .../FilterFollowSetsByHashtag.kt | 23 +++--- .../nip53LiveActivities/SubAssemblyHelper.kt | 5 +- .../FilterLiveActivitiesByAllCommunities.kt | 15 ++-- .../FilterLiveActivitiesByAuthors.kt | 46 ++++++----- .../FilterLiveActivitiesByCommunity.kt | 17 ++-- .../FilterLiveActivitiesByGeohash.kt | 23 +++--- .../FilterLiveActivitiesByHashtag.kt | 23 +++--- .../nip72Communities/SubAssemblyHelper.kt | 5 +- .../FilterCommunitiesByAllCommunities.kt | 15 ++-- .../FilterCommunitiesByAuthors.kt | 46 ++++++----- .../FilterCommunitiesByCommunity.kt | 17 ++-- .../FilterCommunitiesByGeohash.kt | 23 +++--- .../FilterCommunitiesByHashtag.kt | 23 +++--- .../discover/nip90DVMs/SubAssemblyHelper.kt | 5 +- .../FilterContentDVMsByAllCommunities.kt | 15 ++-- .../FilterContentDVMsByAuthors.kt | 46 ++++++----- .../FilterContentDVMsByCommunity.kt | 17 ++-- .../FilterContentDVMsByGeohash.kt | 23 +++--- .../FilterContentDVMsByHashtag.kt | 23 +++--- .../nip99Classifieds/NewProductViewModel.kt | 4 +- .../nip99Classifieds/SubAssemblyHelper.kt | 5 +- .../FilterClassifiedsByAllCommunities.kt | 15 ++-- .../FilterClassifiedsByAuthors.kt | 46 ++++++----- .../FilterClassifiedsByCommunity.kt | 17 ++-- .../FilterClassifiedsByGeohash.kt | 23 +++--- .../FilterClassifiedsByHashtag.kt | 23 +++--- .../loggedIn/geohash/dal/GeoHashFeedFilter.kt | 3 +- .../ui/screen/loggedIn/home/HomeScreen.kt | 7 +- .../loggedIn/home/ShortNotePostViewModel.kt | 4 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 7 +- .../nip01Core/FilterHomePostsByGeohashes.kt | 29 +++---- .../nip01Core/FilterHomePostsByHashtags.kt | 31 +++---- .../nip65Follows/FilterHomePostsByAuthors.kt | 46 ++++++----- .../FilterHomePostsByAllCommunities.kt | 15 ++-- .../FilterHomePostsFromCommunities.kt | 17 ++-- .../datasource/FilterUserProfileFollowers.kt | 5 +- .../datasource/FilterUserProfileLists.kt | 5 +- .../datasource/FilterUserProfileMedia.kt | 5 +- .../datasource/FilterUserProfilePosts.kt | 53 ++++++------ .../FilterUserProfileZapReceived.kt | 5 +- .../relays/common/BasicRelaySetupInfo.kt | 5 +- .../relays/local/LocalRelayListViewModel.kt | 4 +- .../settings/SecurityFiltersScreen.kt | 6 +- .../settings/dal/SpammerAccountsFeedFilter.kt | 4 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 4 +- .../FilterPictureAndVideoByGeohash.kt | 23 +++--- .../FilterPictureAndVideoByHashtag.kt | 23 +++--- .../FilterPictureAndVideoByAuthors.kt | 46 ++++++----- .../FilterPictureAndVideoByAllCommunities.kt | 15 ++-- .../FilterPictureAndVideoByCommunity.kt | 17 ++-- .../ammolite/relays/filters/MutableTime.kt | 4 +- .../relay/client/pool/RelayBasedFilter.kt | 5 +- .../client/single/simple/OutboxProtector.kt | 4 +- .../relay/normalizer/NormalizedRelayUrl.kt | 4 +- .../relay/normalizer/RelayUrlNormalizer.kt | 9 ++- .../quartz/nip10Notes/tags/MarkedETag.kt | 5 +- .../settings/ChatMessageRelayListEvent.kt | 6 +- .../admin/ChannelCreateEvent.kt | 9 +-- .../admin/ChannelMetadataEvent.kt | 9 +-- .../quartz/nip51Lists/MuteListEvent.kt | 5 +- .../quartz/nip51Lists/PeopleListEvent.kt | 5 +- .../RelayListRecommendationProcessor.kt | 10 +-- .../tags/AdvertisedRelayInfoTag.kt | 5 +- .../nip73ExternalIds/topics/HashtagId.kt | 10 +-- .../quartz/utils/MapOfSetBuilder.kt | 5 +- .../quartz/nip01Core/hints/HintIndexerTest.kt | 3 +- 146 files changed, 1151 insertions(+), 1054 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index d4820c0a7e..c430d92ac6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -463,7 +463,18 @@ class Account( if (!isWriteable()) return userMetadata.sendNewUserMetadata( - name, picture, banner, website, pronouns, about, nip05, lnAddress, lnURL, twitter, mastodon, github, + name, + picture, + banner, + website, + pronouns, + about, + nip05, + lnAddress, + lnURL, + twitter, + mastodon, + github, ::sendMyPublicAndPrivateOutbox, ) } @@ -582,11 +593,13 @@ class Account( relay = nip47.relayUri, ) - Amethyst.instance.sources.nwc.subscribe(filter) + Amethyst.instance.sources.nwc + .subscribe(filter) GlobalScope.launch(Dispatchers.IO) { delay(60000) // waits 1 minute to complete payment. - Amethyst.instance.sources.nwc.unsubscribe(filter) + Amethyst.instance.sources.nwc + .unsubscribe(filter) } cache.consume(event, zappedNote, true, nip47.relayUri) { @@ -723,7 +736,11 @@ class Account( if (pubkey == userProfile().pubkeyHex) { notificationRelays.flow.value } else { - LocalCache.getUserIfExists(pubkey)?.inboxRelays()?.ifEmpty { null }?.toSet() + LocalCache + .getUserIfExists(pubkey) + ?.inboxRelays() + ?.ifEmpty { null } + ?.toSet() ?: cache.relayHints.hintsForKey(pubkey).toSet() } } @@ -749,7 +766,10 @@ class Account( outboxRelays.flow.value } else { replyToAuthor.outboxRelays().ifEmpty { null }?.toSet() - ?: cache.relayHints.hintsForKey(replyToAuthor.pubkeyHex).ifEmpty { null }?.toSet() + ?: cache.relayHints + .hintsForKey(replyToAuthor.pubkeyHex) + .ifEmpty { null } + ?.toSet() ?: emptySet() } } else { @@ -776,11 +796,17 @@ class Account( outboxRelays.flow.value } else { author.outboxRelays().ifEmpty { null }?.toSet() - ?: cache.relayHints.hintsForKey(author.pubkeyHex).ifEmpty { null }?.toSet() + ?: cache.relayHints + .hintsForKey(author.pubkeyHex) + .ifEmpty { null } + ?.toSet() ?: emptySet() } } else { - cache.relayHints.hintsForKey(event.pubKey).ifEmpty { null }?.toSet() + cache.relayHints + .hintsForKey(event.pubKey) + .ifEmpty { null } + ?.toSet() emptySet() } @@ -789,7 +815,11 @@ class Account( if (pubkey == userProfile().pubkeyHex) { notificationRelays.flow.value } else { - LocalCache.getUserIfExists(pubkey)?.inboxRelays()?.ifEmpty { null }?.toSet() + LocalCache + .getUserIfExists(pubkey) + ?.inboxRelays() + ?.ifEmpty { null } + ?.toSet() ?: cache.relayHints.hintsForKey(pubkey).toSet() } } @@ -832,7 +862,10 @@ class Account( outboxRelays.flow.value } else { replyToAuthor.outboxRelays().ifEmpty { null }?.toSet() - ?: cache.relayHints.hintsForKey(replyToAuthor.pubkeyHex).ifEmpty { null }?.toSet() + ?: cache.relayHints + .hintsForKey(replyToAuthor.pubkeyHex) + .ifEmpty { null } + ?.toSet() ?: emptySet() } } else { @@ -854,7 +887,10 @@ class Account( outboxRelays.flow.value } else { author.outboxRelays().ifEmpty { null }?.toSet() - ?: cache.relayHints.hintsForKey(author.pubkeyHex).ifEmpty { null }?.toSet() + ?: cache.relayHints + .hintsForKey(author.pubkeyHex) + .ifEmpty { null } + ?.toSet() ?: emptySet() } } else { @@ -866,7 +902,11 @@ class Account( if (pubkey == userProfile().pubkeyHex) { notificationRelays.flow.value } else { - LocalCache.getUserIfExists(pubkey)?.inboxRelays()?.ifEmpty { null }?.toSet() + LocalCache + .getUserIfExists(pubkey) + ?.inboxRelays() + ?.ifEmpty { null } + ?.toSet() ?: cache.relayHints.hintsForKey(pubkey).toSet() } } ?: emptySet() @@ -892,7 +932,10 @@ class Account( outboxRelays.flow.value } else { replyToAuthor.outboxRelays().ifEmpty { null }?.toSet() - ?: cache.relayHints.hintsForKey(replyToAuthor.pubkeyHex).ifEmpty { null }?.toSet() + ?: cache.relayHints + .hintsForKey(replyToAuthor.pubkeyHex) + .ifEmpty { null } + ?.toSet() ?: emptySet() } } else { @@ -1675,7 +1718,11 @@ class Account( if (pubkey == userProfile().pubkeyHex) { notificationRelays.flow.value } else { - LocalCache.getUserIfExists(pubkey)?.inboxRelays()?.ifEmpty { null }?.toSet() + LocalCache + .getUserIfExists(pubkey) + ?.inboxRelays() + ?.ifEmpty { null } + ?.toSet() ?: cache.relayHints.hintsForKey(pubkey).toSet() } } @@ -1888,9 +1935,11 @@ class Account( val relays = nip65RelayList.inboxFlow.value.toSet() NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey.pubkeyHex, signer.pubKey, relays, signer) { val relayList = - dvmPublicKey.inboxRelays().ifEmpty { - LocalCache.relayHints.hintsForKey(dvmPublicKey.pubkeyHex) - }.toSet() + dvmPublicKey + .inboxRelays() + .ifEmpty { + LocalCache.relayHints.hintsForKey(dvmPublicKey.pubkeyHex) + }.toSet() client.send(it, relayList) cache.justConsumeMyOwnEvent(it) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 2933b790f0..4f19f92c06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -250,14 +250,13 @@ class AccountSettings( // --- // proxy settings // --- - fun setTorSettings(newTorSettings: TorSettings): Boolean { - return if (torSettings.update(newTorSettings)) { + fun setTorSettings(newTorSettings: TorSettings): Boolean = + if (torSettings.update(newTorSettings)) { saveAccountSettings() true } else { false } - } // --- // language services diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index 8ee4e293f0..5029f6044f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -114,9 +114,7 @@ class AccountSyncedSettings( } } - fun dontTranslateFromFilteredBySpokenLanguages(): Set { - return languages.dontTranslateFrom - getLanguagesSpokenByUser() - } + fun dontTranslateFromFilteredBySpokenLanguages(): Set = languages.dontTranslateFrom - getLanguagesSpokenByUser() } @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt index 52f9422d31..1117e70fd8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt @@ -59,19 +59,17 @@ class EphemeralChatListState( fun getEphemeralChatList(): EphemeralChatListEvent? = getEphemeralChatListNote().event as? EphemeralChatListEvent - suspend fun ephemeralChatListWithBackup(note: Note): Set { - return ephemeralChatList( + suspend fun ephemeralChatListWithBackup(note: Note): Set = + ephemeralChatList( note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList, ) - } - suspend fun ephemeralChatList(event: EphemeralChatListEvent?): Set { - return tryAndWait { continuation -> + suspend fun ephemeralChatList(event: EphemeralChatListEvent?): Set = + tryAndWait { continuation -> event?.publicAndPrivateRoomIds(signer) { continuation.resume(it) } } ?: emptySet() - } @OptIn(ExperimentalCoroutinesApi::class) val liveEphemeralChatList: StateFlow> by lazy { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt index 88a49482e2..dcdaa2b36a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/localRelays/LocalRelayListState.kt @@ -38,9 +38,7 @@ class LocalRelayListState( val scope: CoroutineScope, val settings: AccountSettings, ) { - fun normalizeLocalRelayListWithBackup(relayList: Set): Set { - return relayList.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }.toSet() - } + fun normalizeLocalRelayListWithBackup(relayList: Set): Set = relayList.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }.toSet() val flow = settings.localRelayServers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt index ad63c5daf8..32bab41605 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt @@ -68,15 +68,18 @@ class FollowListOutboxRelays( @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow> = - kind3Follows.flow.transformLatest { followList -> - val flows: List> = allRelayListFlows(followList.authors) - val relayListFlows = combineAllFlows(flows) - emitAll(relayListFlows) - }.onStart { - kind3Follows.flow.value.authors.mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm() - }.flatten().toSet() - }.flowOn(Dispatchers.Default) + kind3Follows.flow + .transformLatest { followList -> + val flows: List> = allRelayListFlows(followList.authors) + val relayListFlows = combineAllFlows(flows) + emitAll(relayListFlows) + }.onStart { + kind3Follows.flow.value.authors + .mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm() + }.flatten() + .toSet() + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, @@ -85,13 +88,16 @@ class FollowListOutboxRelays( @OptIn(ExperimentalCoroutinesApi::class) val flowSet: StateFlow> = - flow.map { relayList -> - relayList.map { it.url }.toSet() - }.onStart { - kind3Follows.flow.value.authors.mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() - }.flatten().toSet() - }.flowOn(Dispatchers.Default) + flow + .map { relayList -> + relayList.map { it.url }.toSet() + }.onStart { + kind3Follows.flow.value.authors + .mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() + }.flatten() + .toSet() + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt index 89207b4ff8..dfd57f9964 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -71,21 +71,22 @@ class FollowsPerOutboxRelay( @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow>> = - kind3Follows.flow.transformLatest { followList -> - val flows: List> = allRelayListFlows(followList.authors) - val relayListFlows = combineAllFlows(flows) - emitAll(relayListFlows) - }.onStart { - emit( - mapOfSet { - kind3Follows.flow.value.authors.map { authorHex -> - getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay -> - add(relay, authorHex) + kind3Follows.flow + .transformLatest { followList -> + val flows: List> = allRelayListFlows(followList.authors) + val relayListFlows = combineAllFlows(flows) + emitAll(relayListFlows) + }.onStart { + emit( + mapOfSet { + kind3Follows.flow.value.authors.map { authorHex -> + getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay -> + add(relay, authorHex) + } } - } - }, - ) - }.flowOn(Dispatchers.Default) + }, + ) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt index 5dd1f9cd9c..96e6693f21 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt @@ -61,19 +61,17 @@ class PublicChatListState( fun getChannelList(): ChannelListEvent? = getChannelListNote().event as? ChannelListEvent - suspend fun publicChatListWithBackup(note: Note): Set { - return publicChatList( + suspend fun publicChatListWithBackup(note: Note): Set = + publicChatList( note.event as? ChannelListEvent ?: settings.backupChannelList, ) - } - suspend fun publicChatList(event: ChannelListEvent?): Set { - return tryAndWait { continuation -> + suspend fun publicChatList(event: ChannelListEvent?): Set = + tryAndWait { continuation -> event?.publicAndPrivateChannels(signer) { continuation.resume(it) } } ?: emptySet() - } @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow> by lazy { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt index 148cec26b0..e40ef0a77b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt @@ -49,19 +49,17 @@ class BlockPeopleListState( fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent - suspend fun blockListWithBackup(note: Note): PeopleListEvent.UsersAndWords { - return blockList( + suspend fun blockListWithBackup(note: Note): PeopleListEvent.UsersAndWords = + blockList( note.event as? PeopleListEvent, ) - } - suspend fun blockList(event: PeopleListEvent?): PeopleListEvent.UsersAndWords { - return tryAndWait { continuation -> + suspend fun blockList(event: PeopleListEvent?): PeopleListEvent.UsersAndWords = + tryAndWait { continuation -> event?.publicAndPrivateUsersAndWords(signer) { continuation.resume(it) } } ?: PeopleListEvent.UsersAndWords() - } val flow = getBlockListFlow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt index 3464486e4b..3d2e4e6ec2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt @@ -57,19 +57,17 @@ class GeohashListState( fun getGeohashList(): GeohashListEvent? = getGeohashListNote().event as? GeohashListEvent - suspend fun geohashListWithBackup(note: Note): Set { - return geohashList( + suspend fun geohashListWithBackup(note: Note): Set = + geohashList( note.event as? GeohashListEvent ?: settings.backupGeohashList, ) - } - suspend fun geohashList(event: GeohashListEvent?): Set { - return tryAndWait { continuation -> + suspend fun geohashList(event: GeohashListEvent?): Set = + tryAndWait { continuation -> event?.publicAndPrivateGeohash(signer) { continuation.resume(it) } } ?: emptySet() - } @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow> by lazy { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt index 65d18040e0..6b3e09b299 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt @@ -57,19 +57,17 @@ class HashtagListState( fun getHashtagList(): HashtagListEvent? = getHashtagListNote().event as? HashtagListEvent - suspend fun hashtagListWithBackup(note: Note): Set { - return hashtagList( + suspend fun hashtagListWithBackup(note: Note): Set = + hashtagList( note.event as? HashtagListEvent ?: settings.backupHashtagList, ) - } - suspend fun hashtagList(event: HashtagListEvent?): Set { - return tryAndWait { continuation -> + suspend fun hashtagList(event: HashtagListEvent?): Set = + tryAndWait { continuation -> event?.publicAndPrivateHashtag(signer) { continuation.resume(it) } } ?: emptySet() - } @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow> by lazy { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt index 8a953176f1..9888ba5e09 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt @@ -65,14 +65,13 @@ class HiddenUsersState( muteList: PeopleListEvent.UsersAndWords, transientHiddenUsers: Set, showSensitiveContent: Boolean?, - ): LiveHiddenUsers { - return LiveHiddenUsers( + ): LiveHiddenUsers = + LiveHiddenUsers( hiddenUsers = blockList.users + muteList.users, hiddenWords = blockList.words + muteList.words, spammers = transientHiddenUsers, showSensitiveContent = showSensitiveContent, ) - } val flow: StateFlow by lazy { combineTransform( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt index 42e68cda70..c79549def9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt @@ -56,19 +56,17 @@ class MuteListState( fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent - suspend fun muteListWithBackup(note: Note): PeopleListEvent.UsersAndWords { - return muteList( + suspend fun muteListWithBackup(note: Note): PeopleListEvent.UsersAndWords = + muteList( note.event as? MuteListEvent ?: settings.backupMuteList, ) - } - suspend fun muteList(event: MuteListEvent?): PeopleListEvent.UsersAndWords { - return tryAndWait { continuation -> + suspend fun muteList(event: MuteListEvent?): PeopleListEvent.UsersAndWords = + tryAndWait { continuation -> event?.publicAndPrivateUsersAndWords(signer) { continuation.resume(it) } } ?: PeopleListEvent.UsersAndWords() - } val flow = getMuteListFlow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt index 6467b4c7bc..8f37e8cf34 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/OutboxRelaySetState.kt @@ -67,17 +67,20 @@ class OutboxRelaySetState( @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow> = - usersToLoad.transformLatest { followList -> - val flows: List> = allRelayListFlows(followList) - val relayListFlows = combineAllFlows(flows) - emitAll(relayListFlows) - }.onStart { - emit( - usersToLoad.value.mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm() - }.flatten().toSet(), - ) - }.flowOn(Dispatchers.Default) + usersToLoad + .transformLatest { followList -> + val flows: List> = allRelayListFlows(followList) + val relayListFlows = combineAllFlows(flows) + emitAll(relayListFlows) + }.onStart { + emit( + usersToLoad.value + .mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm() + }.flatten() + .toSet(), + ) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Companion.Eagerly, @@ -86,15 +89,18 @@ class OutboxRelaySetState( @OptIn(ExperimentalCoroutinesApi::class) val flowSet: StateFlow> = - flow.map { relayList -> - relayList.map { it.url }.toSet() - }.onStart { - emit( - usersToLoad.value.mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() - }.flatten().toSet(), - ) - }.flowOn(Dispatchers.Default) + flow + .map { relayList -> + relayList.map { it.url }.toSet() + }.onStart { + emit( + usersToLoad.value + .mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() + }.flatten() + .toSet(), + ) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Companion.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt index 375c88034a..84f1271553 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt @@ -60,19 +60,17 @@ class CommunityListState( fun getCommunityList(): CommunityListEvent? = getCommunityListNote().event as? CommunityListEvent - suspend fun communityListWithBackup(note: Note): Set { - return communityList( + suspend fun communityListWithBackup(note: Note): Set = + communityList( note.event as? CommunityListEvent ?: settings.backupCommunityList, ) - } - suspend fun communityList(event: CommunityListEvent?): Set { - return tryAndWait { continuation -> + suspend fun communityList(event: CommunityListEvent?): Set = + tryAndWait { continuation -> event?.publicAndPrivateCommunities(signer) { continuation.resume(it) } } ?: emptySet() - } @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow> by lazy { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt index 663b8adac2..ccee0916ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt @@ -46,15 +46,14 @@ class MergedFollowListsState( hashtages: Set, geohashes: Set, community: Set, - ): FollowListState.Kind3Follows { - return FollowListState.Kind3Follows( + ): FollowListState.Kind3Follows = + FollowListState.Kind3Follows( kind3.authors, kind3.authorsPlusMe, kind3.hashtags + hashtages, kind3.geotags + geohashes, kind3.communities + community.map { it.addressId }, ) - } val flow: StateFlow = combine( @@ -64,18 +63,16 @@ class MergedFollowListsState( communityList.flow, ) { kind3, hashtag, geohash, community -> mergeLists(kind3, hashtag, geohash, community) - } - .onStart { - emit( - mergeLists( - kind3List.flow.value, - hashtagList.flow.value, - geohashList.flow.value, - communityList.flow.value, - ), - ) - } - .flowOn(Dispatchers.Default) + }.onStart { + emit( + mergeLists( + kind3List.flow.value, + hashtagList.flow.value, + geohashList.flow.value, + communityList.flow.value, + ), + ) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index 714c7d23a7..1fdb932662 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -47,9 +47,7 @@ class MergedFollowPlusMineRelayListsState( inbox: Set, private: Set, local: Set, - ): Set { - return kind3 + outbox + inbox + private + local - } + ): Set = kind3 + outbox + inbox + private + local val flow: StateFlow> = combine( @@ -59,19 +57,17 @@ class MergedFollowPlusMineRelayListsState( privateOutboxRelayList.flow, localRelayList.flow, ::mergeLists, - ) - .onStart { - emit( - mergeLists( - followsOutboxRelayList.flow.value, - nip65RelayList.outboxFlow.value, - nip65RelayList.inboxFlow.value, - privateOutboxRelayList.flow.value, - localRelayList.flow.value, - ), - ) - } - .flowOn(Dispatchers.Default) + ).onStart { + emit( + mergeLists( + followsOutboxRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + ), + ) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt index 995ca9a61d..599487b36f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt @@ -62,8 +62,7 @@ class MergedServerListState( val liveServerList: StateFlow> by lazy { combine(fileServers, blossomServers) { nip96s, blossoms -> mergeServerList(nip96s, blossoms) - } - .onStart { emit(mergeServerList(fileServers.value, blossomServers.value)) } + }.onStart { emit(mergeServerList(fileServers.value, blossomServers.value)) } .flowOn(Dispatchers.Default) .stateIn( scope, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt index 95afd53955..ba3a5984ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt @@ -49,9 +49,7 @@ class TrustedRelayListsState( local: Set, dm: Set, search: Set, - ): Set { - return nip65 + private + local + dm + search - } + ): Set = nip65 + private + local + dm + search val flow: StateFlow> = combine( @@ -61,19 +59,17 @@ class TrustedRelayListsState( dmRelayList.flow, searchRelayListState.flow, ::mergeLists, - ) - .onStart { - emit( - mergeLists( - nip65RelayList.allFlow.value, - privateOutboxRelayList.flow.value, - localRelayList.flow.value, - dmRelayList.flow.value, - searchRelayListState.flow.value, - ), - ) - } - .flowOn(Dispatchers.Default) + ).onStart { + emit( + mergeLists( + nip65RelayList.allFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + dmRelayList.flow.value, + searchRelayListState.flow.value, + ), + ) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt index 1d11b9680a..a49b4db544 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt @@ -36,11 +36,12 @@ class CommunityRelayLoader { fun communitiesPerRelay( communityNotes: Array, cache: LocalCache, - ): Map> { - return mapOfSet { + ): Map> = + mapOfSet { communityNotes.forEach { communityNote -> val relays = - (communityNote.note.event as? CommunityDefinitionEvent)?.relayUrls() + (communityNote.note.event as? CommunityDefinitionEvent) + ?.relayUrls() ?.ifEmpty { null } ?: cache.relayHints.hintsForAddress(communityNote.note.idHex) @@ -49,7 +50,6 @@ class CommunityRelayLoader { } } } - } fun communitiesPerRelaySnapshot( communities: Set, @@ -57,9 +57,15 @@ class CommunityRelayLoader { transformation: (Map>) -> T, ): T { val noteMetadata = - communities.mapNotNull { addressId -> - cache.checkGetOrCreateAddressableNote(addressId)?.flow()?.metadata?.stateFlow?.value - }.toTypedArray() + communities + .mapNotNull { addressId -> + cache + .checkGetOrCreateAddressableNote(addressId) + ?.flow() + ?.metadata + ?.stateFlow + ?.value + }.toTypedArray() return transformation(communitiesPerRelay(noteMetadata, cache)) } @@ -70,7 +76,11 @@ class CommunityRelayLoader { ): Flow { val noteMetadataFlows = communities.mapNotNull { addressId -> - cache.checkGetOrCreateAddressableNote(addressId)?.flow()?.metadata?.stateFlow + cache + .checkGetOrCreateAddressableNote(addressId) + ?.flow() + ?.metadata + ?.stateFlow } return combine(noteMetadataFlows) { communityNotes -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index 4f7fa47508..b4dea3cf8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -71,13 +71,12 @@ class FeedTopNavFilterState( @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow = - feedFilterListName.transformLatest { listName -> - emitAll(loadFlowsFor(listName).flow()) - } - .onStart { + feedFilterListName + .transformLatest { listName -> + emitAll(loadFlowsFor(listName).flow()) + }.onStart { loadFlowsFor(feedFilterListName.value).startValue(this) - } - .flowOn(Dispatchers.Default) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt index 8d125d98af..06565733b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt @@ -47,8 +47,8 @@ class MergedTopFeedAuthorListsState( val notificationNavFilter: StateFlow, val scope: CoroutineScope, ) { - fun authorList(navFilter: IFeedTopNavPerRelayFilterSet): Map?> { - return when (navFilter) { + fun authorList(navFilter: IFeedTopNavPerRelayFilterSet): Map?> = + when (navFilter) { is AllCommunitiesTopNavPerRelayFilterSet -> emptyMap() is AllFollowsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } is AuthorsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } @@ -59,15 +59,14 @@ class MergedTopFeedAuthorListsState( is SingleCommunityTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } else -> emptyMap() } - } fun mergeLists( homeNavFilter: IFeedTopNavPerRelayFilterSet, videoNavFilter: IFeedTopNavPerRelayFilterSet, discoveryNavFilter: IFeedTopNavPerRelayFilterSet, notificationNavFilter: IFeedTopNavPerRelayFilterSet, - ): Map> { - return mapOfSet { + ): Map> = + mapOfSet { authorList(homeNavFilter).forEach { (relay, authors) -> authors?.let { add(relay, authors) } } @@ -84,7 +83,6 @@ class MergedTopFeedAuthorListsState( authors?.let { add(relay, authors) } } } - } val flow: StateFlow>> = combine( @@ -102,8 +100,7 @@ class MergedTopFeedAuthorListsState( notificationNavFilter.value, ), ) - } - .flowOn(Dispatchers.Default) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt index 9a68e1e63c..257fdccf03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxLoaderState.kt @@ -40,11 +40,12 @@ class OutboxLoaderState( ) { @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow = - topNavFilter.transformLatest { filterSettings -> - emitAll(filterSettings.toPerRelayFlow(cache)) - }.onStart { - emit(topNavFilter.value.startValue(cache)) - }.flowOn(Dispatchers.Default) + topNavFilter + .transformLatest { filterSettings -> + emitAll(filterSettings.toPerRelayFlow(cache)) + }.onStart { + emit(topNavFilter.value.startValue(cache)) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Companion.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt index 11e520899c..faa7fff9e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt @@ -35,19 +35,20 @@ class OutboxRelayLoader { private fun authorsPerRelay( outboxRelayNotes: Array, cache: LocalCache, - ): Map> { - return mapOfSet { + ): Map> = + mapOfSet { outboxRelayNotes.forEach { outboxNote -> val relays = (outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.ifEmpty { null } - ?: outboxNote.note.author?.pubkeyHex ?.let { cache.relayHints.hintsForKey(it) } + ?: outboxNote.note.author + ?.pubkeyHex + ?.let { cache.relayHints.hintsForKey(it) } relays?.forEach { add(it, outboxNote.note.idHex) } } } - } fun authorsPerRelaySnapshot( authors: Set, @@ -55,9 +56,13 @@ class OutboxRelayLoader { transformation: (Map>) -> T, ): T { val noteMetadata = - authors.map { pubkeyHex -> - cache.getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex)).flow().metadata.stateFlow.value - }.toTypedArray() + authors + .map { pubkeyHex -> + cache + .getOrCreateAddressableNote(AdvertisedRelayListEvent.createAddress(pubkeyHex)) + .flow() + .metadata.stateFlow.value + }.toTypedArray() return transformation(authorsPerRelay(noteMetadata, cache)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt index 2d6515f079..b840ce75df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt @@ -54,12 +54,10 @@ class AllFollowsByOutboxTopNavFilter( val geotagScopes: Set? = geotags?.mapTo(mutableSetOf()) { GeohashId.Companion.toScope(it) } val hashtagScopes: Set? = hashtags?.mapTo(mutableSetOf()) { HashtagId.Companion.toScope(it) } - override fun matchAuthor(pubkey: HexKey): Boolean { - return authors == null || pubkey in authors - } + override fun matchAuthor(pubkey: HexKey): Boolean = authors == null || pubkey in authors - override fun match(noteEvent: Event): Boolean { - return if (noteEvent is LiveActivitiesEvent) { + override fun match(noteEvent: Event): Boolean = + if (noteEvent is LiveActivitiesEvent) { (authors != null && noteEvent.participantsIntersect(authors)) || (hashtags != null && noteEvent.isTaggedHashes(hashtags)) || (geotags != null && noteEvent.isTaggedGeoHashes(geotags)) || @@ -78,7 +76,6 @@ class AllFollowsByOutboxTopNavFilter( (geotags != null && noteEvent.isTaggedGeoHashes(geotags)) || (communities != null && noteEvent.isTaggedAddressableNotes(communities)) } - } override fun toPerRelayFlow(cache: LocalCache): Flow { val authorsPerRelay = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt index ed162f657a..8052cb2767 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt @@ -32,8 +32,8 @@ class AllFollowsFeedFlow( val allFollows: StateFlow, val followsRelays: StateFlow>, ) : IFeedFlowsType { - fun convert(kind3: FollowListState.Kind3Follows?): AllFollowsByOutboxTopNavFilter { - return if (kind3 != null) { + fun convert(kind3: FollowListState.Kind3Follows?): AllFollowsByOutboxTopNavFilter = + if (kind3 != null) { AllFollowsByOutboxTopNavFilter( authors = kind3.authors, hashtags = kind3.hashtags, @@ -47,7 +47,6 @@ class AllFollowsFeedFlow( defaultRelays = followsRelays, ) } - } override fun flow() = allFollows.map(::convert) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt index 51207c0286..28db5545f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt @@ -32,8 +32,8 @@ class AroundMeFeedFlow( val location: StateFlow, val allFollowRelays: StateFlow>, ) : IFeedFlowsType { - fun convert(result: LocationState.LocationResult): LocationTopNavFilter { - return if (result is LocationState.LocationResult.Success) { + fun convert(result: LocationState.LocationResult): LocationTopNavFilter = + if (result is LocationState.LocationResult.Success) { // 2 neighbors deep = 25x25km LocationTopNavFilter( geotags = compute50kmRange(result.geoHash).toSet(), @@ -46,7 +46,6 @@ class AroundMeFeedFlow( relays = allFollowRelays, ) } - } override fun flow() = location.map(::convert) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt index 3016de1622..641f3a521c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt @@ -53,13 +53,10 @@ class LocationTopNavFilter( } } - override fun toPerRelayFlow(cache: LocalCache): Flow { - return relays.map { + override fun toPerRelayFlow(cache: LocalCache): Flow = + relays.map { LocationTopNavPerRelayFilterSet(it.associateWith { LocationTopNavPerRelayFilter(geotags) }) } - } - override fun startValue(cache: LocalCache): LocationTopNavPerRelayFilterSet { - return LocationTopNavPerRelayFilterSet(relays.value.associateWith { LocationTopNavPerRelayFilter(geotags) }) - } + override fun startValue(cache: LocalCache): LocationTopNavPerRelayFilterSet = LocationTopNavPerRelayFilterSet(relays.value.associateWith { LocationTopNavPerRelayFilter(geotags) }) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt index a8ab72b43c..64a53643c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt @@ -38,15 +38,13 @@ class GlobalTopNavFilter( override fun match(noteEvent: Event) = true - override fun toPerRelayFlow(cache: LocalCache): Flow { - return relays.map { + override fun toPerRelayFlow(cache: LocalCache): Flow = + relays.map { GlobalTopNavPerRelayFilterSet(it.associateWith { GlobalTopNavPerRelayFilter }) } - } - override fun startValue(cache: LocalCache): GlobalTopNavPerRelayFilterSet { - return GlobalTopNavPerRelayFilterSet( + override fun startValue(cache: LocalCache): GlobalTopNavPerRelayFilterSet = + GlobalTopNavPerRelayFilterSet( relays.value.associateWith { GlobalTopNavPerRelayFilter }, ) - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt index c261a34581..d13b66bed9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt @@ -42,27 +42,24 @@ class HashtagTopNavFilter( override fun matchAuthor(pubkey: HexKey): Boolean = true - override fun match(noteEvent: Event): Boolean { - return if (noteEvent is CommentEvent) { + override fun match(noteEvent: Event): Boolean = + if (noteEvent is CommentEvent) { noteEvent.isTaggedHashes(hashtags) || noteEvent.isTaggedScopes(hashtagScopes) } else { noteEvent.isTaggedHashes(hashtags) } - } - override fun toPerRelayFlow(cache: LocalCache): Flow { - return relays.map { + override fun toPerRelayFlow(cache: LocalCache): Flow = + relays.map { HashtagTopNavPerRelayFilterSet( it.associateWith { HashtagTopNavPerRelayFilter(hashtags) }, ) } - } - override fun startValue(cache: LocalCache): HashtagTopNavPerRelayFilterSet { - return HashtagTopNavPerRelayFilterSet( + override fun startValue(cache: LocalCache): HashtagTopNavPerRelayFilterSet = + HashtagTopNavPerRelayFilterSet( relays.value.associateWith { HashtagTopNavPerRelayFilter(hashtags) }, ) - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt index 0dca95f15a..741ef90688 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt @@ -36,24 +36,20 @@ class AllCommunitiesTopNavFilter( ) : IFeedTopNavFilter { override fun matchAuthor(pubkey: HexKey): Boolean = true - override fun match(noteEvent: Event): Boolean { - return noteEvent.isTaggedAddressableNotes(communities) - } + override fun match(noteEvent: Event): Boolean = noteEvent.isTaggedAddressableNotes(communities) fun convert(map: Map>) = AllCommunitiesTopNavPerRelayFilterSet( map.mapValues { AllCommunitiesTopNavPerRelayFilter(it.value) }, ) - override fun toPerRelayFlow(cache: LocalCache): Flow { - return CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) { + override fun toPerRelayFlow(cache: LocalCache): Flow = + CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) { convert(it) } - } - override fun startValue(cache: LocalCache): AllCommunitiesTopNavPerRelayFilterSet { - return CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) { + override fun startValue(cache: LocalCache): AllCommunitiesTopNavPerRelayFilterSet = + CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) { convert(it) } - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt index 14697499b0..e6b2e5c1ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt @@ -36,24 +36,19 @@ class AuthorsByOutboxTopNavFilter( ) : IFeedTopNavFilter { override fun matchAuthor(pubkey: HexKey) = pubkey in authors - override fun match(noteEvent: Event): Boolean { - return if (noteEvent is LiveActivitiesEvent) { + override fun match(noteEvent: Event): Boolean = + if (noteEvent is LiveActivitiesEvent) { noteEvent.participantsIntersect(authors) } else { noteEvent.pubKey in authors } - } fun convert(map: Map>) = AuthorsByOutboxTopNavPerRelayFilterSet( map.mapValues { AuthorsByOutboxTopNavPerRelayFilter(it.value) }, ) - override fun toPerRelayFlow(cache: LocalCache): Flow { - return OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert) - } + override fun toPerRelayFlow(cache: LocalCache): Flow = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert) - override fun startValue(cache: LocalCache): AuthorsByOutboxTopNavPerRelayFilterSet { - return OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert) - } + override fun startValue(cache: LocalCache): AuthorsByOutboxTopNavPerRelayFilterSet = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt index 3d70161929..273d373b80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt @@ -41,15 +41,14 @@ class SingleCommunityTopNavFilter( ) : IFeedTopNavFilter { override fun matchAuthor(pubkey: HexKey) = authors == null || pubkey in authors - override fun match(noteEvent: Event): Boolean { - return if (noteEvent is LiveActivitiesEvent) { + override fun match(noteEvent: Event): Boolean = + if (noteEvent is LiveActivitiesEvent) { (authors != null && noteEvent.participantsIntersect(authors)) || noteEvent.isTaggedAddressableNote(community) } else if (noteEvent is CommentEvent) { (authors != null && noteEvent.pubKey in authors) || noteEvent.isTaggedAddressableNote(community) } else { (authors != null && noteEvent.pubKey in authors) || noteEvent.isTaggedAddressableNote(community) } - } override fun toPerRelayFlow(cache: LocalCache): Flow { // relay field takes priority diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt index d98676d6d9..d90db485db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt @@ -36,24 +36,19 @@ class MutedAuthorsByOutboxTopNavFilter( ) : IFeedTopNavFilter { override fun matchAuthor(pubkey: HexKey) = pubkey in authors - override fun match(noteEvent: Event): Boolean { - return if (noteEvent is LiveActivitiesEvent) { + override fun match(noteEvent: Event): Boolean = + if (noteEvent is LiveActivitiesEvent) { noteEvent.participantsIntersect(authors) } else { noteEvent.pubKey in authors } - } fun convert(map: Map>) = MutedAuthorsByOutboxTopNavPerRelayFilterSet( map.mapValues { MutedAuthorsByOutboxTopNavPerRelayFilter(it.value) }, ) - override fun toPerRelayFlow(cache: LocalCache): Flow { - return OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert) - } + override fun toPerRelayFlow(cache: LocalCache): Flow = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert) - override fun startValue(cache: LocalCache): MutedAuthorsByOutboxTopNavPerRelayFilterSet { - return OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert) - } + override fun startValue(cache: LocalCache): MutedAuthorsByOutboxTopNavPerRelayFilterSet = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt index 0699da58bc..c20e5793fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownTopNavFilter.kt @@ -36,9 +36,7 @@ class UnknownTopNavFilter( override fun match(noteEvent: Event) = false - override fun toPerRelayFlow(cache: LocalCache): Flow { - return MutableStateFlow(UnknownTopNavPerRelayFilterSet) - } + override fun toPerRelayFlow(cache: LocalCache): Flow = MutableStateFlow(UnknownTopNavPerRelayFilterSet) override fun startValue(cache: LocalCache) = UnknownTopNavPerRelayFilterSet } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt index db2277eb80..1041e69ccf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayEvaluation.kt @@ -30,8 +30,8 @@ class TorRelayEvaluation( val trustedRelayList: Set, val dmRelayList: Set, ) { - fun useTor(relay: NormalizedRelayUrl): Boolean { - return if (torSettings.torType == TorType.OFF) { + fun useTor(relay: NormalizedRelayUrl): Boolean = + if (torSettings.torType == TorType.OFF) { false } else { if (relay.isLocalHost()) { @@ -46,5 +46,4 @@ class TorRelayEvaluation( torSettings.newRelaysViaTor } } - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt index daa8384486..3358e4699a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt @@ -48,11 +48,11 @@ class TorRelayState( settings.torSettings.trustedRelaysViaTor, settings.torSettings.newRelaysViaTor, ) { - torType: TorType, - onionRelaysViaTor: Boolean, - dmRelaysViaTor: Boolean, - trustedRelaysViaTor: Boolean, - newRelaysViaTor: Boolean, + torType: TorType, + onionRelaysViaTor: Boolean, + dmRelaysViaTor: Boolean, + trustedRelaysViaTor: Boolean, + newRelaysViaTor: Boolean, -> TorRelaySettings(torType, onionRelaysViaTor, dmRelaysViaTor, trustedRelaysViaTor, newRelaysViaTor) }.onStart { @@ -65,8 +65,7 @@ class TorRelayState( settings.torSettings.newRelaysViaTor.value, ), ) - } - .flowOn(Dispatchers.Default) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index 844a5e6e1d..505fb75f99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -175,15 +175,14 @@ class ZapPaymentHandler( val user: User? = null, ) - fun receivingRelaySet(userHex: HexKey): Set? { - return ( + fun receivingRelaySet(userHex: HexKey): Set? = + ( LocalCache .getAddressableNoteIfExists( AdvertisedRelayListEvent.createAddressTag(userHex), )?.event as? AdvertisedRelayListEvent - ) - ?.readRelaysNorm()?.toSet() - } + )?.readRelaysNorm() + ?.toSet() suspend fun signAllZapRequests( note: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt index 61133f089f..37af51d960 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/ProxySettingsAnchor.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class ProxySettingsAnchor() { +class ProxySettingsAnchor { val flow: MutableStateFlow> = MutableStateFlow( MutableStateFlow( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt index 4881062a95..fdc54672d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt @@ -53,16 +53,17 @@ class RelayProxyClientConnector( connManager.status, ) { torSettings, torConnection, clearConnection, connectivity -> torSettings.hashCode() + torConnection.hashCode() + clearConnection.hashCode() + connectivity.hashCode() - }.debounce(100).onEach { - Log.d("ManageRelayServices", "Relay Services have changed") - client.reconnect(true) - }.onStart { - Log.d("ManageRelayServices", "Resuming Relay Services") - client.connect() - }.onCompletion { - Log.d("ManageRelayServices", "Pausing Relay Services") - client.disconnect() - }.flowOn(Dispatchers.IO) + }.debounce(100) + .onEach { + Log.d("ManageRelayServices", "Relay Services have changed") + client.reconnect(true) + }.onStart { + Log.d("ManageRelayServices", "Resuming Relay Services") + client.connect() + }.onCompletion { + Log.d("ManageRelayServices", "Pausing Relay Services") + client.disconnect() + }.flowOn(Dispatchers.IO) .stateIn( scope, SharingStarted.WhileSubscribed(30000), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt index 3bcc0e0390..0622fac81d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt @@ -43,15 +43,14 @@ class AccountGiftWrapsEoseManager( override fun updateFilter( key: AccountQueryState, since: SincePerRelayMap?, - ): List { - return key.account.dmRelays.flow.value.flatMap { relay -> + ): List = + key.account.dmRelays.flow.value.flatMap { relay -> filterGiftWrapsToPubkey( relay = relay, pubkey = user(key).pubkeyHex, since = since?.get(relay)?.time, ) } - } val userJobMap = mutableMapOf>() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt index 6aa98492bc..3a5bd44aba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt @@ -38,15 +38,14 @@ class LiveActivityWatcherSubAssembly( override fun updateFilter( key: ChannelFinderQueryState, since: SincePerRelayMap?, - ): List { - return if (key.channel is LiveActivitiesChannel) { + ): List = + if (key.channel is LiveActivitiesChannel) { key.channel.relays().flatMap { filterLiveStreamUpdatesByAddress(it, listOf(key.channel), since?.get(it)?.time) } } else { emptyList() } - } /** * Only one key per channel. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt index 483ff830bd..36cff0b093 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt @@ -80,8 +80,10 @@ class EventWatcherSubAssembler( notes: Iterable, eoseCache: EOSEAccountFast, ): Collection> = - notes.groupBy { eoseCache.since(it)?.keys?.hashCode() } - .values.map { + notes + .groupBy { eoseCache.since(it)?.keys?.hashCode() } + .values + .map { // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again it.sortedBy { it.idHex } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt index f43380b2f3..7ce042af79 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt @@ -36,11 +36,10 @@ val MetadataAndRelayListKinds = AdvertisedRelayListEvent.KIND, ) -fun filterFindUserMetadataForKey(author: HexKey): List { - return LocalCache.checkGetOrCreateUser(author)?.let { +fun filterFindUserMetadataForKey(author: HexKey): List = + LocalCache.checkGetOrCreateUser(author)?.let { filterFindUserMetadataForKey(setOf(it)) } ?: emptyList() -} fun filterFindUserMetadataForKey(authors: Set): List { val perRelayKeys = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt index 049e3f9cfc..ebcd0f627a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterUserMetadataForKey.kt @@ -45,14 +45,16 @@ fun filterUserMetadataForKey( since: SincePerRelayMap?, ): List { val relays = - authors.map { - val authorHomeRelayEventAddress = AdvertisedRelayListEvent.createAddressTag(it) - val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) + authors + .map { + val authorHomeRelayEventAddress = AdvertisedRelayListEvent.createAddressTag(it) + val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) - authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null } - ?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null } - ?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay) - }.flatten().toSet() + authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null } + ?: LocalCache.relayHints.hintsForKey(it).ifEmpty { null } + ?: listOfNotNull(LocalCache.getUserIfExists(it)?.latestMetadataRelay) + }.flatten() + .toSet() return relays.map { RelayBasedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt index 15b82ce307..4288337a21 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MediaCompressor.kt @@ -108,7 +108,7 @@ class MediaCompressor { // videoName = "compressed_video" // => required name // ), // OR AND NOT BOTH - appSpecificStorageConfiguration = AppSpecificStorageConfiguration(), + storageConfiguration = AppSpecificStorageConfiguration(), configureWith = Configuration( quality = videoQuality, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 65d19f6d86..199e2d236a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -129,7 +129,11 @@ fun EditPostView( val scrollState = rememberScrollState() val scope = rememberCoroutineScope() var showRelaysDialog by remember { mutableStateOf(false) } - var relayList = remember { accountViewModel.account.outboxRelays.flow.value.toImmutableList() } + var relayList = + remember { + accountViewModel.account.outboxRelays.flow.value + .toImmutableList() + } LaunchedEffect(Unit) { postViewModel.load(edit, versionLookingAt, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index b0e2824c3f..fb81e50822 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -101,7 +101,11 @@ fun NewMediaView( } var showRelaysDialog by remember { mutableStateOf(false) } - var relayList = remember { accountViewModel.account.outboxRelays.flow.value.toImmutableList() } + var relayList = + remember { + accountViewModel.account.outboxRelays.flow.value + .toImmutableList() + } Dialog( onDismissRequest = { onClose() }, @@ -202,7 +206,8 @@ fun ImageVideoPost( accountViewModel: AccountViewModel, ) { val nip95description = stringRes(id = R.string.upload_server_relays_nip95) - val fileServers by accountViewModel.account.serverLists.liveServerList.collectAsState() + val fileServers by accountViewModel.account.serverLists.liveServerList + .collectAsState() val fileServerOptions = remember(fileServers) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt index 1db3eaffe4..10a0446c6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt @@ -30,9 +30,7 @@ import kotlinx.serialization.encoding.Encoder object NormalizedRelayUrlSerializer : KSerializer { override val descriptor = PrimitiveSerialDescriptor("NormalizedRelayUrl", PrimitiveKind.STRING) - override fun deserialize(decoder: Decoder): NormalizedRelayUrl { - return NormalizedRelayUrl(decoder.decodeString()) - } + override fun deserialize(decoder: Decoder): NormalizedRelayUrl = NormalizedRelayUrl(decoder.decodeString()) override fun serialize( encoder: Encoder, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 2db170fbe5..8f3b353c55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -139,7 +139,8 @@ class UpdateZapAmountViewModel : ViewModel() { ?.let { TextFieldValue(it) } ?: TextFieldValue("") this.walletConnectRelay = myAccount.settings.zapPaymentRequest - ?.relayUri?.url + ?.relayUri + ?.url ?.let { TextFieldValue(it) } ?: TextFieldValue("") this.walletConnectSecret = myAccount.settings.zapPaymentRequest diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index c2c9eeb778..cf3dae4e1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -373,7 +373,8 @@ fun WatchUserFollows( if (accountViewModel.isLoggedUser(userHex)) { onFollowChanges(true) } else { - val state by accountViewModel.account.kind3FollowList.flow.collectAsStateWithLifecycle() + val state by accountViewModel.account.kind3FollowList.flow + .collectAsStateWithLifecycle() onFollowChanges(state.authors.contains(userHex)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt index baf2356eaa..a119bc8a94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt @@ -98,7 +98,8 @@ fun ImageVideoDescription( ) { val nip95description = stringRes(id = R.string.upload_server_relays_nip95) - val fileServers by accountViewModel.account.serverLists.liveServerList.collectAsState() + val fileServers by accountViewModel.account.serverLists.liveServerList + .collectAsState() val fileServerOptions = remember(fileServers, includeNIP95) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index 752ac98dfa..04bb1a78c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -50,7 +50,8 @@ fun DisplayFollowingHashtagsInPost( accountViewModel: AccountViewModel, nav: INav, ) { - val userFollowState by accountViewModel.account.allFollows.flow.collectAsStateWithLifecycle() + val userFollowState by accountViewModel.account.allFollows.flow + .collectAsStateWithLifecycle() var firstTag by remember(baseNote) { mutableStateOf(null) } LaunchedEffect(key1 = userFollowState) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 6cebce6135..1b049286bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -498,13 +498,14 @@ open class CommentPostViewModel : multiOrchestrator = null } else { val errorMessages = - results.errors.map { - stringRes( - context, - it.errorResource, - *it.params, - ) - }.distinct() + results.errors + .map { + stringRes( + context, + it.errorResource, + *it.params, + ) + }.distinct() onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } @@ -555,7 +556,9 @@ open class CommentPostViewModel : fun reloadRelaySet() { val account = accountViewModel?.account ?: return - relayList = account.outboxRelays.flow.value.toImmutableList() + relayList = + account.outboxRelays.flow.value + .toImmutableList() } fun deleteMediaToUpload(selected: SelectedMediaProcessing) { @@ -626,7 +629,10 @@ open class CommentPostViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare( item.link.url, - { Amethyst.Companion.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, + { + Amethyst.Companion.instance.okHttpClients + .getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) + }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 55f6cb25a9..299d4cc0a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -361,7 +361,8 @@ fun WatchAddressableNoteFollows( accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit, ) { - val state by accountViewModel.account.kind3FollowList.flow.collectAsStateWithLifecycle() + val state by accountViewModel.account.kind3FollowList.flow + .collectAsStateWithLifecycle() onFollowChanges(state.communities.contains(note.idHex)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 0810a02ea5..6056e32b11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -167,7 +167,9 @@ class AccountViewModel( .flow() .relays.stateFlow .map { contactListState -> - contactListState.user.latestContactList?.relays()?.keys ?: emptySet() + contactListState.user.latestContactList + ?.relays() + ?.keys ?: emptySet() }.flowOn(Dispatchers.Default) .stateIn( viewModelScope, @@ -1664,27 +1666,29 @@ class AccountViewModel( fun relayStatusFlow() = app.client.relayStatusFlow() - fun convertAccounts(loggedInAccounts: List?): Set { - return loggedInAccounts?.mapNotNull { - try { - it.npub.bechToBytes().toHexKey() - } catch (e: Exception) { - if (e is CancellationException) throw e - null - } - }?.toSet() ?: emptySet() - } + fun convertAccounts(loggedInAccounts: List?): Set = + loggedInAccounts + ?.mapNotNull { + try { + it.npub.bechToBytes().toHexKey() + } catch (e: Exception) { + if (e is CancellationException) throw e + null + } + }?.toSet() ?: emptySet() val trustedAccounts: StateFlow> = - LocalPreferences.accountsFlow().map { loggedInAccounts -> - convertAccounts(loggedInAccounts) - }.onStart { - emit(convertAccounts(LocalPreferences.allSavedAccounts())) - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(5000), - emptySet(), - ) + LocalPreferences + .accountsFlow() + .map { loggedInAccounts -> + convertAccounts(loggedInAccounts) + }.onStart { + emit(convertAccounts(LocalPreferences.allSavedAccounts())) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + emptySet(), + ) val draftNoteCache = CachedDraftNotes(this) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index 2c6389ec22..65df1f06f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -130,7 +130,8 @@ fun ObserveAntiSpamFilterSettings(accountViewModel: AccountViewModel) { @Composable fun SetProxyDeterminator(accountViewModel: AccountViewModel) { LaunchedEffect(accountViewModel) { - Amethyst.instance.torProxySettingsAnchor.flow.tryEmit(accountViewModel.account.torRelayState.flow) + Amethyst.instance.torProxySettingsAnchor.flow + .tryEmit(accountViewModel.account.torRelayState.flow) } } @@ -143,7 +144,8 @@ fun ObserveImageLoadingTor(accountViewModel: AccountViewModel) { @Composable fun ManageRelayServices(accountViewModel: AccountViewModel) { - val relayServices by Amethyst.instance.relayProxyClientConnector.relayServices.collectAsStateWithLifecycle() + val relayServices by Amethyst.instance.relayProxyClientConnector.relayServices + .collectAsStateWithLifecycle() Log.d("ManageRelayServices", "Relay Services changed $relayServices") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt index 82fb3225e4..9a5525d291 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatMetaViewModel.kt @@ -44,7 +44,8 @@ class NewEphemeralChatMetaViewModel : ViewModel() { } fun buildRoom() = - RelayUrlNormalizer.normalizeOrNull(relayUrl.value.text) + RelayUrlNormalizer + .normalizeOrNull(relayUrl.value.text) ?.let { RoomId(channelName.value.text, it) } /* diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt index 59b1288c7d..a7723ac357 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt @@ -50,30 +50,31 @@ fun filterLastMessageFollowingPublicChats( } } - return relayRoomDTags.map { - listOf( - RelayBasedFilter( - // Metadata comes from any relay - relay = it.key, - filter = - Filter( - kinds = listOf(ChannelMetadataEvent.KIND), - tags = mapOf("e" to it.value.sorted()), - since = since?.get(it.key)?.time, - limit = 1, - ), - ), - RelayBasedFilter( - relay = it.key, - filter = - Filter( - kinds = listOf(ChannelMessageEvent.KIND), - tags = mapOf("e" to it.value.sorted()), - since = since?.get(it.key)?.time, - // Remember to consider spam that is being removed from the UI - limit = 100, - ), - ), - ) - }.flatten() + return relayRoomDTags + .map { + listOf( + RelayBasedFilter( + // Metadata comes from any relay + relay = it.key, + filter = + Filter( + kinds = listOf(ChannelMetadataEvent.KIND), + tags = mapOf("e" to it.value.sorted()), + since = since?.get(it.key)?.time, + limit = 1, + ), + ), + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = listOf(ChannelMessageEvent.KIND), + tags = mapOf("e" to it.value.sorted()), + since = since?.get(it.key)?.time, + // Remember to consider spam that is being removed from the UI + limit = 100, + ), + ), + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt index afadd3b355..e32888b580 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsFromMe.kt @@ -30,8 +30,8 @@ fun filterNip04DMsFromMe( user: User, relay: NormalizedRelayUrl, since: Long?, -): RelayBasedFilter { - return RelayBasedFilter( +): RelayBasedFilter = + RelayBasedFilter( relay = relay, filter = Filter( @@ -40,4 +40,3 @@ fun filterNip04DMsFromMe( since = since, ), ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt index b86aa6280b..2488bed77f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterNip04DMsToMe.kt @@ -30,8 +30,8 @@ fun filterNip04DMsToMe( user: User, relay: NormalizedRelayUrl, since: Long?, -): RelayBasedFilter { - return RelayBasedFilter( +): RelayBasedFilter = + RelayBasedFilter( relay = relay, filter = Filter( @@ -40,4 +40,3 @@ fun filterNip04DMsToMe( since = since, ), ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt index b7065770a1..87ac292f1f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt @@ -148,7 +148,8 @@ private fun ImageVideoPostChat( fileUploadState: ChatFileUploadState, accountViewModel: AccountViewModel, ) { - val fileServers by accountViewModel.account.serverLists.liveServerList.collectAsState() + val fileServers by accountViewModel.account.serverLists.liveServerList + .collectAsState() val fileServerOptions = remember(fileServers) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt index e0447aae2a..c728a95267 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/FilterCommunityPosts.kt @@ -30,8 +30,8 @@ fun filterCommunityPosts( relay: NormalizedRelayUrl, community: CommunityDefinitionEvent, since: Long?, -): RelayBasedFilter { - return RelayBasedFilter( +): RelayBasedFilter = + RelayBasedFilter( relay = relay, filter = Filter( @@ -42,4 +42,3 @@ fun filterCommunityPosts( since = since, ), ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt index 9083c8e909..aea7bdacff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt @@ -42,8 +42,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeLongFormFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, -): List { - return when (feedSettings) { +): List = + when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterLongFormByAllCommunities(feedSettings, since) is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterLongFormByFollows(feedSettings, since) is AuthorsByOutboxTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since) @@ -54,4 +54,3 @@ fun makeLongFormFilter( is SingleCommunityTopNavPerRelayFilterSet -> filterLongFormByCommunity(feedSettings, since) else -> emptyList() } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt index f08543c749..1d62339373 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt @@ -75,11 +75,12 @@ fun filterLongFormByAllCommunities( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterLongFormAllCommunities( - relay = it.key, - communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterLongFormAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt index 838d31e009..40f3f914de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt @@ -60,17 +60,18 @@ fun filterLongFormByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterLongFormAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterLongFormAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } fun filterLongFormByAuthors( @@ -81,15 +82,16 @@ fun filterLongFormByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterLongFormAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterLongFormAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt index 81570692bd..7cb866ad01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt @@ -77,12 +77,13 @@ fun filterLongFormByCommunity( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterLongFormByCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterLongFormByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt index 51f81e1379..138a4fca2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt @@ -61,15 +61,16 @@ fun filterLongFormByGeohash( val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set.mapNotNull { - if (it.value.geotags.isEmpty()) { - null - } else { - filterLiveActivitiesByGeohash( - relay = it.key, - geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return geoSet.set + .mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterLiveActivitiesByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt index 71afa309c7..07c36e21be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt @@ -61,15 +61,16 @@ fun filterLongFormByHashtag( val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set.mapNotNull { relayHashSet -> - if (relayHashSet.value.hashtags.isEmpty()) { - null - } else { - filterLongFormByHashtag( - relay = relayHashSet.key, - hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, - ) - } - }.flatten() + return hashSet.set + .mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterLongFormByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt index d3db8579c1..4b79b94a2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt @@ -42,8 +42,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makePublicChatsFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, -): List { - return when (feedSettings) { +): List = + when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterPublicChatsByAllCommunities(feedSettings, since) is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByFollows(feedSettings, since) is AuthorsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since) @@ -54,4 +54,3 @@ fun makePublicChatsFilter( is SingleCommunityTopNavPerRelayFilterSet -> filterPublicChatsByCommunity(feedSettings, since) else -> emptyList() } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt index 130409fa62..656e396f33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt @@ -82,11 +82,12 @@ fun filterPublicChatsByAllCommunities( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterPublicChatsAllCommunities( - relay = it.key, - communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterPublicChatsAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt index 3aedb18754..aa3a7a7f8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt @@ -67,17 +67,18 @@ fun filterPublicChatsByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterPublicChatsAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterPublicChatsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } fun filterPublicChatsByAuthors( @@ -88,15 +89,16 @@ fun filterPublicChatsByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterPublicChatsAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterPublicChatsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt index ce0975292b..98d183db45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt @@ -84,12 +84,13 @@ fun filterPublicChatsByCommunity( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterPublicChatsByCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterPublicChatsByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt index fafc069a5d..2e03857e5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt @@ -67,15 +67,16 @@ fun filterPublicChatsByGeohash( val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set.mapNotNull { - if (it.value.geotags.isEmpty()) { - null - } else { - filterPublicChatsByGeohash( - relay = it.key, - geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return geoSet.set + .mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterPublicChatsByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt index d7a69d40bf..1f70a7a73b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt @@ -68,15 +68,16 @@ fun filterPublicChatsByHashtag( val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set.mapNotNull { relayHashSet -> - if (relayHashSet.value.hashtags.isEmpty()) { - null - } else { - filterPublicChatsByHashtag( - relay = relayHashSet.key, - hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, - ) - } - }.flatten() + return hashSet.set + .mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterPublicChatsByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt index bb5a3f60a3..8c3797df9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt @@ -42,8 +42,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeFollowSetsFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, -): List { - return when (feedSettings) { +): List = + when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterFollowSetsByAllCommunities(feedSettings, since) is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByFollows(feedSettings, since) is AuthorsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since) @@ -54,4 +54,3 @@ fun makeFollowSetsFilter( is SingleCommunityTopNavPerRelayFilterSet -> filterFollowSetsByCommunity(feedSettings, since) else -> emptyList() } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt index 21ec68b6cf..93ca8f70e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt @@ -76,11 +76,12 @@ fun filterFollowSetsByAllCommunities( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterContentDVMsAllCommunities( - relay = it.key, - communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterContentDVMsAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt index d37e3e3920..3d08502c2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt @@ -60,17 +60,18 @@ fun filterFollowSetsByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterFollowSetsAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterFollowSetsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } fun filterFollowSetsByAuthors( @@ -81,15 +82,16 @@ fun filterFollowSetsByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterFollowSetsAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterFollowSetsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt index 7a0fb3f66b..09a9be8984 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt @@ -77,12 +77,13 @@ fun filterFollowSetsByCommunity( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterFollowSetsByCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterFollowSetsByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt index 1ab2287ab9..21195dd0ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt @@ -60,15 +60,16 @@ fun filterFollowSetsByGeohash( val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set.mapNotNull { - if (it.value.geotags.isEmpty()) { - null - } else { - filterFollowSetsByGeohash( - relay = it.key, - geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return geoSet.set + .mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterFollowSetsByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt index 530015ce04..f5e8e4caf0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt @@ -61,15 +61,16 @@ fun filterFollowSetsByHashtag( val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set.mapNotNull { relayHashSet -> - if (relayHashSet.value.hashtags.isEmpty()) { - null - } else { - filterFollowSetsByHashtag( - relay = relayHashSet.key, - hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, - ) - } - }.flatten() + return hashSet.set + .mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterFollowSetsByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt index 13acbd03ee..0cca444f94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt @@ -42,8 +42,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeLiveActivitiesFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, -): List { - return when (feedSettings) { +): List = + when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterLiveActivitiesByAllCommunities(feedSettings, since) is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByFollows(feedSettings, since) is AuthorsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since) @@ -54,4 +54,3 @@ fun makeLiveActivitiesFilter( is SingleCommunityTopNavPerRelayFilterSet -> filterLiveActivitiesByCommunity(feedSettings, since) else -> emptyList() } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt index c139cea3d7..6354b18c61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt @@ -77,11 +77,12 @@ fun filterLiveActivitiesByAllCommunities( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterContentDVMsAllCommunities( - relay = it.key, - communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterContentDVMsAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt index 84225a667e..8d32112266 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt @@ -72,17 +72,18 @@ fun filterLiveActivitiesByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterLiveActivitiesAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterLiveActivitiesAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } fun filterLiveActivitiesByAuthors( @@ -93,15 +94,16 @@ fun filterLiveActivitiesByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterLiveActivitiesAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterLiveActivitiesAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt index 382c0d6332..0141bb8b70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt @@ -78,12 +78,13 @@ fun filterLiveActivitiesByCommunity( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterLiveActivitiesByCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterLiveActivitiesByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt index 8a9470a0c7..875bf036b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt @@ -61,15 +61,16 @@ fun filterLiveActivitiesByGeohash( val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set.mapNotNull { - if (it.value.geotags.isEmpty()) { - null - } else { - filterLiveActivitiesByGeohash( - relay = it.key, - geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return geoSet.set + .mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterLiveActivitiesByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt index 32bd409ca1..5e7e7b4bdb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt @@ -62,15 +62,16 @@ fun filterLiveActivitiesByHashtag( val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set.mapNotNull { relayHashSet -> - if (relayHashSet.value.hashtags.isEmpty()) { - null - } else { - filterLiveActivitiesByHashtag( - relay = relayHashSet.key, - hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, - ) - } - }.flatten() + return hashSet.set + .mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterLiveActivitiesByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt index 97576f15ef..4b275fae7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt @@ -42,8 +42,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeCommunitiesFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, -): List { - return when (feedSettings) { +): List = + when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterCommunitiesByAllCommunities(feedSettings, since) is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByFollows(feedSettings, since) is AuthorsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since) @@ -54,4 +54,3 @@ fun makeCommunitiesFilter( is SingleCommunityTopNavPerRelayFilterSet -> filterCommunitiesByCommunity(feedSettings, since) else -> emptyList() } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt index 3dff4e38ca..07f1637a0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt @@ -71,11 +71,12 @@ fun filterCommunitiesByAllCommunities( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterCommunitiesAllCommunities( - relay = it.key, - communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterCommunitiesAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt index b1dab2df3e..7557077b84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt @@ -61,17 +61,18 @@ fun filterCommunitiesByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterCommunitiesAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterCommunitiesAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } fun filterCommunitiesByAuthors( @@ -82,15 +83,16 @@ fun filterCommunitiesByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterCommunitiesAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterCommunitiesAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt index 4a1bc7b7e0..6697fd72c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt @@ -60,12 +60,13 @@ fun filterCommunitiesByCommunity( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterClassifiedsByCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterClassifiedsByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt index 338fb283f8..a3f87648d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt @@ -61,15 +61,16 @@ fun filterCommunitiesByGeohash( val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set.mapNotNull { - if (it.value.geotags.isEmpty()) { - null - } else { - filterCommunitiesByGeohash( - relay = it.key, - geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return geoSet.set + .mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterCommunitiesByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt index 0a6aa4afdb..59dc98a02c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt @@ -62,15 +62,16 @@ fun filterCommunitiesByHashtag( val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set.mapNotNull { relayHashSet -> - if (relayHashSet.value.hashtags.isEmpty()) { - null - } else { - filterCommunitiesByHashtag( - relay = relayHashSet.key, - hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, - ) - } - }.flatten() + return hashSet.set + .mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterCommunitiesByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt index 3438e6ef22..1a03971c5f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt @@ -42,8 +42,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeContentDVMsFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, -): List { - return when (feedSettings) { +): List = + when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterContentDVMsByAllCommunities(feedSettings, since) is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByFollows(feedSettings, since) is AuthorsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since) @@ -54,4 +54,3 @@ fun makeContentDVMsFilter( is SingleCommunityTopNavPerRelayFilterSet -> filterContentDVMsByCommunity(feedSettings, since) else -> emptyList() } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt index 04b0963091..0af5469578 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt @@ -75,11 +75,12 @@ fun filterContentDVMsByAllCommunities( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterContentDVMsAllCommunities( - relay = it.key, - communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterContentDVMsAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt index 2c7cfa0ae6..cd5769e97c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt @@ -61,17 +61,18 @@ fun filterContentDVMsByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterContentDVMsAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterContentDVMsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } fun filterContentDVMsByAuthors( @@ -82,15 +83,16 @@ fun filterContentDVMsByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterContentDVMsAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterContentDVMsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt index 8c0e586d1e..1cdb07be4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt @@ -77,12 +77,13 @@ fun filterContentDVMsByCommunity( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterContentDVMsByCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterContentDVMsByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt index 0f8a6e7de6..23f3342b39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt @@ -60,15 +60,16 @@ fun filterContentDVMsByGeohash( val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set.mapNotNull { - if (it.value.geotags.isEmpty()) { - null - } else { - filterContentDVMsByGeohash( - relay = it.key, - geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return geoSet.set + .mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterContentDVMsByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt index 1a486d0118..615c014384 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt @@ -61,15 +61,16 @@ fun filterContentDVMsByHashtag( val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set.mapNotNull { relayHashSet -> - if (relayHashSet.value.hashtags.isEmpty()) { - null - } else { - filterContentDVMsByHashtag( - relay = relayHashSet.key, - hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, - ) - } - }.flatten() + return hashSet.set + .mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterContentDVMsByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 56ce892d31..3648ad00c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -475,7 +475,9 @@ open class NewProductViewModel : fun reloadRelaySet() { val account = accountViewModel?.account ?: return - relayList = account.outboxRelays.flow.value.toImmutableList() + relayList = + account.outboxRelays.flow.value + .toImmutableList() } fun deleteMediaToUpload(selected: SelectedMediaProcessing) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt index b4047919a0..fca10fdc3f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt @@ -42,8 +42,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeClassifiedsFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, -): List { - return when (feedSettings) { +): List = + when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterClassifiedsByAllCommunities(feedSettings, since) is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByFollows(feedSettings, since) is AuthorsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since) @@ -54,4 +54,3 @@ fun makeClassifiedsFilter( is SingleCommunityTopNavPerRelayFilterSet -> filterClassifiedsByCommunity(feedSettings, since) else -> emptyList() } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt index d143e23bc4..3ba7cd6193 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt @@ -75,11 +75,12 @@ fun filterClassifiedsByAllCommunities( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterClassifiedsAllCommunities( - relay = it.key, - communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterClassifiedsAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt index c6168c634f..838c6f04e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt @@ -60,17 +60,18 @@ fun filterClassifiedsByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterClassifiedsAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterClassifiedsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } fun filterClassifiedsByAuthors( @@ -81,15 +82,16 @@ fun filterClassifiedsByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterClassifiedsAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterClassifiedsAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt index 7b5d64e708..1ee093d0cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt @@ -77,12 +77,13 @@ fun filterClassifiedsByCommunity( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterClassifiedsByCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterClassifiedsByCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt index f43b661216..5225b0a2f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt @@ -60,15 +60,16 @@ fun filterClassifiedsByGeohash( val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set.mapNotNull { - if (it.value.geotags.isEmpty()) { - null - } else { - filterClassifiedsByGeohash( - relay = it.key, - geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return geoSet.set + .mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterClassifiedsByGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt index 5efa7423d7..8aa3322181 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt @@ -61,15 +61,16 @@ fun filterClassifiedsByHashtag( val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set.mapNotNull { relayHashSet -> - if (relayHashSet.value.hashtags.isEmpty()) { - null - } else { - filterClassifiedsByHashtag( - relay = relayHashSet.key, - hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, - ) - } - }.flatten() + return hashSet.set + .mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterClassifiedsByHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt index 1ffdfaefd5..a0f556e32b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/dal/GeoHashFeedFilter.kt @@ -79,7 +79,8 @@ class GeoHashFeedFilter( event is PrivateDmEvent || event is PollNoteEvent || event is AudioHeaderEvent - ) && event.isTaggedGeoHash(geohash) + ) && + event.isTaggedGeoHash(geohash) fun acceptableViaScope( event: Event?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 5d3deaef4d..87dd27e364 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -228,10 +228,13 @@ fun HomeScreenFloatingButton( accountViewModel: AccountViewModel, nav: INav, ) { - val list = accountViewModel.account.settings.defaultHomeFollowList.collectAsStateWithLifecycle() + val list = + accountViewModel.account.settings.defaultHomeFollowList + .collectAsStateWithLifecycle() if (list.value == AROUND_ME) { - val location by Amethyst.instance.locationManager.geohashStateFlow.collectAsStateWithLifecycle() + val location by Amethyst.instance.locationManager.geohashStateFlow + .collectAsStateWithLifecycle() when (val myLocation = location) { is LocationState.LocationResult.Success -> NewGeoPostButton(myLocation.geoHash.toString(), accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 2ef0869e05..90d5280f4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -716,7 +716,9 @@ open class ShortNotePostViewModel : fun reloadRelaySet() { val account = accountViewModel?.account ?: return - relayList = account.outboxRelays.flow.value.toImmutableList() + relayList = + account.outboxRelays.flow.value + .toImmutableList() } fun deleteMediaToUpload(selected: SelectedMediaProcessing) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index e4b15204ea..7005e70167 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -68,9 +68,10 @@ class HomeLiveFilter( filterParams: FilterByListParams, timeLimit: Long, ): Boolean = - channel.notes.filter { key, value -> - acceptableEvent(value, filterParams, timeLimit) - }.isNotEmpty() + channel.notes + .filter { key, value -> + acceptableEvent(value, filterParams, timeLimit) + }.isNotEmpty() override fun updateListWith( oldList: List, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt index 6297505617..12b90b80a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt @@ -82,20 +82,21 @@ fun filterHomePostsByGeohashes( val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set.mapNotNull { - if (it.value.geotags.isEmpty()) { - null - } else { - filterHomePostsByGeohashes( - relay = it.key, - geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, - ) + - filterHomePostsByScopes( + return geoSet.set + .mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterHomePostsByGeohashes( relay = it.key, - scopesToLoad = it.value.geotagScopes, + geotags = it.value.geotags, since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + ) + + filterHomePostsByScopes( + relay = it.key, + scopesToLoad = it.value.geotagScopes, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt index f7bbd8a74b..fb65c4851d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt @@ -86,22 +86,23 @@ fun filterHomePostsByHashtags( val defaultSince = TimeUtils.oneWeekAgo() - return hashtagSet.set.mapNotNull { - if (it.value.hashtags.isEmpty()) { - null - } else { - val since = since?.get(it.key)?.time ?: defaultSince + return hashtagSet.set + .mapNotNull { + if (it.value.hashtags.isEmpty()) { + null + } else { + val since = since?.get(it.key)?.time ?: defaultSince - return filterHomePostsByHashtags( - relay = it.key, - hashToLoad = it.value.hashtags, - since = since, - ) + - filterHomePostsByScopes( + return filterHomePostsByHashtags( relay = it.key, - scopesToLoad = it.value.hashtagScopes, + hashToLoad = it.value.hashtags, since = since, - ) - } - }.flatten() + ) + + filterHomePostsByScopes( + relay = it.key, + scopesToLoad = it.value.hashtagScopes, + since = since, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt index 386cf8afbf..4fd05a4bb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -107,17 +107,18 @@ fun filterHomePostsByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterHomePostsByAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterHomePostsByAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } fun filterHomePostsByAuthors( @@ -128,15 +129,16 @@ fun filterHomePostsByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterHomePostsByAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterHomePostsByAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt index 431091725c..0eaf28b7fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt @@ -74,11 +74,12 @@ fun filterHomePostsByAllCommunities( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterHomePostsFromAllCommunities( - relay = it.key, - communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterHomePostsFromAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt index 337a1cb124..30a90c8578 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt @@ -108,12 +108,13 @@ fun filterHomePostsByCommunity( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterHomePostsFromCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterHomePostsFromCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt index 19ad86a314..274fc334bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileFollowers.kt @@ -31,8 +31,8 @@ val UserProfileFollowersKinds = listOf(ContactListEvent.KIND) fun filterUserProfileFollowers( user: User, since: SincePerRelayMap?, -): List { - return user.inboxRelays().map { +): List = + user.inboxRelays().map { RelayBasedFilter( relay = it, filter = @@ -43,4 +43,3 @@ fun filterUserProfileFollowers( ), ) } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt index e1ebad1cb6..51258fad51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt @@ -40,8 +40,8 @@ val UserProfileListKinds = fun filterUserProfileLists( users: Map>, since: SincePerRelayMap?, -): List { - return users.map { +): List = + users.map { RelayBasedFilter( relay = it.key, filter = @@ -53,4 +53,3 @@ fun filterUserProfileLists( ), ) } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt index 80dc2ec2c5..a1a97d3320 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMedia.kt @@ -40,8 +40,8 @@ val UserProfileMediaKinds = fun filterUserProfileMedia( user: User, since: SincePerRelayMap?, -): List { - return user.outboxRelays().map { relay -> +): List = + user.outboxRelays().map { relay -> RelayBasedFilter( relay = relay, filter = @@ -53,4 +53,3 @@ fun filterUserProfileMedia( ), ) } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt index 43bd81fe9c..24e3068655 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt @@ -60,29 +60,30 @@ val UserProfilePostKinds2 = fun filterUserProfilePosts( user: User, since: SincePerRelayMap?, -): List { - return user.outboxRelays().map { relay -> - listOf( - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = UserProfilePostKinds1, - authors = listOf(user.pubkeyHex), - limit = 200, - since = since?.get(relay)?.time, - ), - ), - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = UserProfilePostKinds2, - authors = listOf(user.pubkeyHex), - limit = 50, - since = since?.get(relay)?.time, - ), - ), - ) - }.flatten() -} +): List = + user + .outboxRelays() + .map { relay -> + listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = UserProfilePostKinds1, + authors = listOf(user.pubkeyHex), + limit = 200, + since = since?.get(relay)?.time, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = UserProfilePostKinds2, + authors = listOf(user.pubkeyHex), + limit = 50, + since = since?.get(relay)?.time, + ), + ), + ) + }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt index f9abf7c275..ffd9635425 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileZapReceived.kt @@ -31,8 +31,8 @@ val UserProfileZapReceiverKinds = listOf(LnZapEvent.KIND) fun filterUserProfileZapsReceived( user: User, since: SincePerRelayMap?, -): List { - return user.inboxRelays().map { relay -> +): List = + user.inboxRelays().map { relay -> RelayBasedFilter( relay = relay, filter = @@ -44,4 +44,3 @@ fun filterUserProfileZapsReceived( ), ) } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt index 1f13e3d636..c176d97507 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt @@ -32,9 +32,8 @@ data class BasicRelaySetupInfo( val paidRelay: Boolean = false, ) -fun relaySetupInfoBuilder(normalized: NormalizedRelayUrl): BasicRelaySetupInfo { - return BasicRelaySetupInfo( +fun relaySetupInfoBuilder(normalized: NormalizedRelayUrl): BasicRelaySetupInfo = + BasicRelaySetupInfo( normalized, RelayStats.get(normalized), ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt index 615668f819..9cabbb3bd3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt @@ -24,7 +24,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class LocalRelayListViewModel : BasicRelaySetupInfoModel() { - override fun getRelayList(): List = account.localRelayList.flow.value.toList() + override fun getRelayList(): List = + account.localRelayList.flow.value + .toList() override fun saveRelayList(urlList: List) { account.localRelayList.saveRelayList(urlList) {} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 2981f85d9e..cb49a840a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -335,8 +335,10 @@ fun WatchAccountAndBlockList( accountViewModel: AccountViewModel, invalidate: () -> Unit, ) { - val transientSpammers by accountViewModel.account.hiddenUsers.transientHiddenUsers.collectAsStateWithLifecycle() - val blockListState by accountViewModel.account.hiddenUsers.flow.collectAsStateWithLifecycle() + val transientSpammers by accountViewModel.account.hiddenUsers.transientHiddenUsers + .collectAsStateWithLifecycle() + val blockListState by accountViewModel.account.hiddenUsers.flow + .collectAsStateWithLifecycle() LaunchedEffect(accountViewModel, transientSpammers, blockListState) { invalidate() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt index 69a19a8bdd..542fa1f921 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/dal/SpammerAccountsFeedFilter.kt @@ -32,5 +32,7 @@ class SpammerAccountsFeedFilter( override fun showHiddenKey(): Boolean = true - override fun feed(): List = account.hiddenUsers.transientHiddenUsers.value.map { LocalCache.getOrCreateUser(it) } + override fun feed(): List = + account.hiddenUsers.transientHiddenUsers.value + .map { LocalCache.getOrCreateUser(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index e8f5f14c05..e925c9be17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -164,7 +164,9 @@ fun WatchAccountForVideoScreen( accountViewModel: AccountViewModel, ) { val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle() - val hiddenUsers = accountViewModel.account.hiddenUsers.flow.collectAsStateWithLifecycle() + val hiddenUsers = + accountViewModel.account.hiddenUsers.flow + .collectAsStateWithLifecycle() LaunchedEffect(accountViewModel, listState, hiddenUsers) { videoFeedContentState.checkKeysInvalidateDataAndSendToTop() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt index 277c21e6bc..5e8d242fab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt @@ -72,15 +72,16 @@ fun filterPictureAndVideoByGeohash( ): List { if (geoSet.set.isEmpty()) return emptyList() - return geoSet.set.mapNotNull { - if (it.value.geotags.isEmpty()) { - null - } else { - filterPictureAndVideoGeohash( - relay = it.key, - geotags = it.value.geotags, - since = since?.get(it.key)?.time, - ) - } - }.flatten() + return geoSet.set + .mapNotNull { + if (it.value.geotags.isEmpty()) { + null + } else { + filterPictureAndVideoGeohash( + relay = it.key, + geotags = it.value.geotags, + since = since?.get(it.key)?.time, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt index 12dc9003e1..7c40f44526 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt @@ -75,15 +75,16 @@ fun filterPictureAndVideoByHashtag( val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set.mapNotNull { relayHashSet -> - if (relayHashSet.value.hashtags.isEmpty()) { - null - } else { - filterPictureAndVideoHashtag( - relay = relayHashSet.key, - hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, - ) - } - }.flatten() + return hashSet.set + .mapNotNull { relayHashSet -> + if (relayHashSet.value.hashtags.isEmpty()) { + null + } else { + filterPictureAndVideoHashtag( + relay = relayHashSet.key, + hashtags = relayHashSet.value.hashtags, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt index 7b490dcad9..25e1423955 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt @@ -73,17 +73,18 @@ fun filterPictureAndVideoByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterPictureAndVideoAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterPictureAndVideoAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } fun filterPictureAndVideoByAuthors( @@ -94,15 +95,16 @@ fun filterPictureAndVideoByAuthors( val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set.mapNotNull { - if (it.value.authors.isEmpty()) { - null - } else { - filterPictureAndVideoAuthors( - relay = it.key, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - } - }.flatten() + return authorSet.set + .mapNotNull { + if (it.value.authors.isEmpty()) { + null + } else { + filterPictureAndVideoAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + } + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt index 08071cb909..997bdb254c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt @@ -107,11 +107,12 @@ fun filterPictureAndVideoByAllCommunities( ): List { if (communitySet.set.isEmpty()) return emptyList() - return communitySet.set.mapNotNull { - filterPictureAndVideoAllCommunities( - relay = it.key, - communities = it.value.communities, - since = since?.get(it.key)?.time, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterPictureAndVideoAllCommunities( + relay = it.key, + communities = it.value.communities, + since = since?.get(it.key)?.time, + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt index 61aee43e22..a6c532b049 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt @@ -113,12 +113,13 @@ fun filterPictureAndVideoByCommunity( val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set.mapNotNull { - filterPictureAndVideoCommunity( - relay = it.key, - community = it.value.community, - authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, - ) - }.flatten() + return communitySet.set + .mapNotNull { + filterPictureAndVideoCommunity( + relay = it.key, + community = it.value.community, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: defaultSince, + ) + }.flatten() } diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/MutableTime.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/MutableTime.kt index 155f628c59..67dfd8f2f4 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/MutableTime.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/filters/MutableTime.kt @@ -23,7 +23,9 @@ package com.vitorpamplona.ammolite.relays.filters /* * Wrapper class to allow changing in EOSE without modifying the list it is included within */ -class MutableTime(startTime: Long) { +class MutableTime( + startTime: Long, +) { var time: Long = startTime private set diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt index d4922d889b..2383936e74 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt @@ -33,11 +33,10 @@ class RelayBasedFilter( // This only exists because some relays confuse empty lists with null lists fun isValidFor(relay: NormalizedRelayUrl) = relay == this.relay - fun toFilter(relay: NormalizedRelayUrl): Filter? { - return if (isValidFor(relay)) { + fun toFilter(relay: NormalizedRelayUrl): Filter? = + if (isValidFor(relay)) { filter } else { null } - } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt index 80d1d378ed..9e1884b9ab 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt @@ -29,7 +29,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.utils.LargeCache -class OutboxProtector(listener: IRelayClientListener) : RedirectRelayClientListener(listener) { +class OutboxProtector( + listener: IRelayClientListener, +) : RedirectRelayClientListener(listener) { /** * Auth procedures require us to keep track of the outgoing events * to make sure the relay waits for the auth to finish and send them. diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt index 38abf28e72..c74f03348e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.normalizer -data class NormalizedRelayUrl(val url: String) : Comparable { +data class NormalizedRelayUrl( + val url: String, +) : Comparable { override fun compareTo(other: NormalizedRelayUrl) = url.compareTo(other.url) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index f42e8b7d85..8d3a84549e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -31,7 +31,14 @@ class RelayUrlNormalizer { fun isOnion(url: String) = url.endsWith(".onion") || url.endsWith(".onion/") - private fun norm(url: String) = NormalizedRelayUrl(URIReference.parse(url).normalize().toString().intern()) + private fun norm(url: String) = + NormalizedRelayUrl( + URIReference + .parse(url) + .normalize() + .toString() + .intern(), + ) fun fix(url: String): String { val trimmed = url.trim() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt index 2c04df5d1e..ece41e8b21 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt @@ -59,15 +59,14 @@ data class MarkedETag( ; companion object { - fun parse(code: String): MARKER? { - return when (code) { + fun parse(code: String): MARKER? = + when (code) { ROOT.code -> ROOT REPLY.code -> REPLY MENTION.code -> MENTION FORK.code -> FORK else -> null } - } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt index ac5a6eb2b5..40c9338344 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt @@ -58,8 +58,7 @@ class ChatMessageRelayListEvent( RelayTag.assemble(it) }.plusElement( AltTag.assemble("Relay list to receive private messages"), - ) - .toTypedArray() + ).toTypedArray() fun updateRelayList( earlierVersion: ChatMessageRelayListEvent, @@ -69,7 +68,8 @@ class ChatMessageRelayListEvent( onReady: (ChatMessageRelayListEvent) -> Unit, ) { val tags = - earlierVersion.tags.filter(RelayTag::notMatch) + earlierVersion.tags + .filter(RelayTag::notMatch) .plus( relays.map { RelayTag.assemble(it) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index d7925bd075..c7164a668b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -46,16 +46,15 @@ class ChannelCreateEvent( sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider { - override fun eventHints() = channelInfo().relays?.mapNotNull { EventIdHint(id, it) } ?: emptyList() + override fun eventHints() = channelInfo().relays?.map { EventIdHint(id, it) } ?: emptyList() - fun channelInfo(): ChannelDataNorm { - return try { + fun channelInfo(): ChannelDataNorm = + try { ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() } catch (e: JsonParseException) { - Log.e("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) + Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) ChannelDataNorm() } - } companion object { const val KIND = 40 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt index 456bca8bc3..d46c0ece8e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt @@ -48,16 +48,15 @@ class ChannelMetadataEvent( sig: HexKey, ) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider { - override fun eventHints() = channelInfo().relays?.mapNotNull { EventIdHint(id, it) } ?: emptyList() + override fun eventHints() = channelInfo().relays?.map { EventIdHint(id, it) } ?: emptyList() - fun channelInfo(): ChannelDataNorm { - return try { + fun channelInfo(): ChannelDataNorm = + try { ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() } catch (e: JsonParseException) { - Log.e("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) + Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) ChannelDataNorm() } - } companion object { const val KIND = 41 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt index 4536f28bd2..16719a9722 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt @@ -61,13 +61,12 @@ class MuteListEvent( } } - suspend fun publicAndPrivateUsersAndWords(signer: NostrSigner): UsersAndWords? { - return tryAndWait { continuation -> + suspend fun publicAndPrivateUsersAndWords(signer: NostrSigner): UsersAndWords? = + tryAndWait { continuation -> publicAndPrivateUsersAndWords(signer) { privateTagList -> continuation.resume(privateTagList) } } - } companion object { const val KIND = 10000 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt index 3e35ff9f55..08a80ad1a6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt @@ -64,13 +64,12 @@ class PeopleListEvent( } } - suspend fun publicAndPrivateUsersAndWords(signer: NostrSigner): UsersAndWords? { - return tryAndWait { continuation -> + suspend fun publicAndPrivateUsersAndWords(signer: NostrSigner): UsersAndWords? = + tryAndWait { continuation -> publicAndPrivateUsersAndWords(signer) { privateTagList -> continuation.resume(privateTagList) } } - } fun isTaggedWord( word: String, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt index fd0953e566..5856ed926f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/RelayListRecommendationProcessor.kt @@ -31,8 +31,8 @@ class RelayListRecommendationProcessor { fun transpose( userList: Map>, ignore: Set = setOf(), - ): Map> { - return mapOfSet { + ): Map> = + mapOfSet { userList.forEach { event -> event.value.forEach { relay -> if (relay !in ignore) { @@ -41,7 +41,6 @@ class RelayListRecommendationProcessor { } } } - } /** * filter onion and local host from write relays @@ -50,8 +49,8 @@ class RelayListRecommendationProcessor { private fun filterValidRelays( userList: List, hasOnionConnection: Boolean = false, - ): Map> { - return mapOfSet { + ): Map> = + mapOfSet { userList.forEach { event -> event.writeRelaysNorm()?.forEach { relay -> if (!relay.isLocalHost() && (hasOnionConnection || !relay.isOnion())) { @@ -60,7 +59,6 @@ class RelayListRecommendationProcessor { } } } - } fun reliableRelaySetFor( usersAndRelays: Map>, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt index 07d6a13194..4bb8d237b3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt @@ -94,13 +94,12 @@ class AdvertisedRelayInfo( fun assemble( relay: NormalizedRelayUrl, type: AdvertisedRelayType, - ): Array { - return if (type == AdvertisedRelayType.BOTH) { + ): Array = + if (type == AdvertisedRelayType.BOTH) { arrayOf(TAG_NAME, relay.url) } else { arrayOf(TAG_NAME, relay.url, type.code) } - } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/topics/HashtagId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/topics/HashtagId.kt index 50b4be0a34..166d3a2554 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/topics/HashtagId.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/topics/HashtagId.kt @@ -70,23 +70,21 @@ class HashtagId( return encoded.indexOf(value, 1) > 0 } - fun parse(encoded: String): String? { - return if (encoded.length > 1 && encoded[0] == KIND_CHR) { + fun parse(encoded: String): String? = + if (encoded.length > 1 && encoded[0] == KIND_CHR) { encoded.substring(1) } else { null } - } fun parse( encoded: String, hint: String?, - ): HashtagId? { - return if (encoded.length > 1 && encoded[0] == KIND_CHR) { + ): HashtagId? = + if (encoded.length > 1 && encoded[0] == KIND_CHR) { HashtagId(encoded.substring(1), hint) } else { null } - } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt index 698daf72bf..95fbae954c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt @@ -64,10 +64,9 @@ fun mapOfSet(init: MapOfSetBuilder.() -> Unit): Map> { return data.build() } -fun merge(maps: List>>): Map> { - return mapOfSet { +fun merge(maps: List>>): Map> = + mapOfSet { maps.forEach { map -> add(map) } } -} diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt index 4a265ad020..f58a4cf7da 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexerTest.kt @@ -63,7 +63,8 @@ class HintIndexerTest { .getResourceAsStream("relayDB.txt") ?.readAllBytes() .toString() - .split("\n").mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) } + .split("\n") + .mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) } val indexer by lazy { System.gc() From bb55aea4a400b8d6d7362542e8b7b9f3c1a93db4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 12:50:17 -0400 Subject: [PATCH 007/185] Fixing log messages --- .../amethyst/model/AntiSpamFilter.kt | 2 +- .../amethyst/model/LocalCache.kt | 4 +-- .../service/notifications/RegisterAccounts.kt | 6 ++--- .../nip01Core/relay/client/NostrClient.kt | 26 ------------------- .../client/single/basic/BasicRelayClient.kt | 20 +++++++------- 5 files changed, 16 insertions(+), 42 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index c1c433c1c7..731951eadc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -74,7 +74,7 @@ class AntiSpamFilter { ) { Log.w( "Potential SPAM Message", - "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} $relay ${event.content.replace("\n", " | ")}", + "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}", ) // Log down offenders diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index a2977894f6..89e2c535d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2947,7 +2947,7 @@ object LocalCache : ILocalCache { // update relay with deletion event from another. if (relay != null) { deletionIndex.hasBeenDeletedBy(event)?.let { - Log.d("LocalCache", "Updating ${relay.url} with a Deletion Event ${it.toJson()} because of ${event.toJson()}") + Log.d("LocalCache", "Updating ${relay.url.url} with a Deletion Event ${it.toJson()} because of ${event.toJson()}") relay.send(it) } } @@ -2959,7 +2959,7 @@ object LocalCache : ILocalCache { getAddressableNoteIfExists(event.addressTag())?.let { note -> note.event?.let { existingEvent -> if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay.url)) { - Log.d("LocalCache", "Updating ${relay.url} with a new version of ${event.toJson()} to ${existingEvent.toJson()}") + Log.d("LocalCache", "Updating ${relay.url.url} with a new version of ${event.toJson()} to ${existingEvent.toJson()}") relay.send(existingEvent) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index b5ce69cc4b..bac952a2c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -102,15 +102,15 @@ class RegisterAccounts( if (acc != null && acc.isWriteable()) { val nip65Read = acc.backupNIP65RelayList?.readRelaysNorm() ?: emptyList() - Log.d(tag, "Register Account ${it.npub} NIP65 Reads ${nip65Read.joinToString(", ")}") + Log.d(tag, "Register Account ${it.npub} NIP65 Reads ${nip65Read.joinToString(", ") { it.url } }") val nip17Read = acc.backupDMRelayList?.relays() ?: emptyList() - Log.d(tag, "Register Account ${it.npub} NIP17 Reads ${nip17Read.joinToString(", ")}") + Log.d(tag, "Register Account ${it.npub} NIP17 Reads ${nip17Read.joinToString(", ") { it.url } }") val readKind3Relays = acc.backupContactList?.relays()?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList() - Log.d(tag, "Register Account ${it.npub} Kind3 Reads ${readKind3Relays.joinToString(", ")}") + Log.d(tag, "Register Account ${it.npub} Kind3 Reads ${readKind3Relays.joinToString(", ") { it.url } }") val relays = (nip65Read + nip17Read + readKind3Relays) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index ce1eeca333..c9500f43bd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -72,32 +72,6 @@ class NostrClient( activeSubscriptions.relays.value + eventOutbox.relays.value }.onEach { relayPool.updatePool(it) - - val subs = activeSubscriptions.allSubscriptions() - - it.forEach { relay -> - var hasSub = false - subs.forEach { sub -> - sub.value.filters.forEach { - if (it.isValidFor(relay)) { - hasSub = true - } - } - } - if (!hasSub) { - Log.d("NostrClient", "AABBCC $relay doesn't have any sub") - } - - if (relayPool.getRelay(relay)?.isConnected() == false) { - Log.d("NostrClient", "AABBCC $relay is not connected") - } - } - - val filters = subs.values.sumOf { it.filters.size } - Log.d("NostrClient", "Updating list for relays to ${it.size} relays and ${subs.size} subscriptions and $filters filters") - it.forEach { - Log.d("NostrClient", "${it.url} isConnected ${relayPool.getRelay(it)?.isConnected()}") - } }.flowOn(Dispatchers.Default) .stateIn( scope, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 1aed379245..2194df618d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -98,8 +98,6 @@ open class BasicRelayClient( } fun connectAndRunOverride(onConnected: () -> Unit) { - Log.d("Relay", "Relay.connect $url isAlreadyConnecting: ${connectingMutex.get()}") - // If there is a connection, don't wait. if (connectingMutex.getAndSet(true)) { return @@ -111,6 +109,8 @@ open class BasicRelayClient( return } + Log.d("Relay", "${url.url} connecting...") + lastConnectTentative = TimeUtils.now() socket = socketBuilder.build(url, MyWebsocketListener(onConnected)) @@ -118,7 +118,7 @@ open class BasicRelayClient( } catch (e: Exception) { if (e is CancellationException) throw e - Log.w("Relay", "Relay Crash before connedting $url", e) + Log.w("Relay", "${url.url} Crash before connecting $url", e) stats.newError(e.message ?: "Error trying to connect: ${e.javaClass.simpleName}") markConnectionAsClosed() @@ -135,7 +135,7 @@ open class BasicRelayClient( pingMillis: Long, compression: Boolean, ) { - Log.d("Relay", "Connect onOpen $url $socket") + Log.d("Relay", "${url.url} onConnect $socket") markConnectionAsReady(pingMillis, compression) @@ -161,7 +161,7 @@ open class BasicRelayClient( } catch (e: Throwable) { if (e is CancellationException) throw e stats.newError("Error processing: $text") - Log.e("Relay", "Error processing: $text from ${url.url}") + Log.e("Relay", "${url.url} Error processing: $text") listener.onError(this@BasicRelayClient, "", Error("Error processing $text")) } } @@ -170,7 +170,7 @@ open class BasicRelayClient( code: Int, reason: String, ) { - Log.w("Relay", "Relay onClosing $url: $reason") + Log.w("Relay", "${url.url} onClosing: $code $reason") listener.onRelayStateChange(this@BasicRelayClient, RelayState.DISCONNECTING) } @@ -181,7 +181,7 @@ open class BasicRelayClient( ) { markConnectionAsClosed() - Log.w("Relay", "Relay onClosed $url: $reason") + Log.w("Relay", "${url.url} onClosed $reason") listener.onRelayStateChange(this@BasicRelayClient, RelayState.DISCONNECTED) } @@ -201,7 +201,7 @@ open class BasicRelayClient( // Failures disconnect the relay. markConnectionAsClosed() - Log.w("Relay", "Relay onFailure $url, $code $response ${t.message} $socket") + Log.w("Relay", "${url.url} onFailure $code $response ${t.message} $socket") t.printStackTrace() listener.onError( this@BasicRelayClient, @@ -258,7 +258,7 @@ open class BasicRelayClient( msg: OkMessage, onConnected: () -> Unit, ) { - Log.w("Relay", "Relay on OK $url, ${msg.eventId} ${msg.success} ${msg.message}") + Log.w("Relay", "${url.url} Relay on OK: ${msg.eventId} ${msg.success} ${msg.message}") // if this is the OK of an auth event, renew all subscriptions and resend all outgoing events. if (authResponseWatcher.containsKey(msg.eventId)) { @@ -300,7 +300,7 @@ open class BasicRelayClient( } override fun disconnect() { - Log.d("Relay", "Relay.disconnect $url") + Log.d("Relay", "${url.url} disconnecting...") lastConnectTentative = 0L // this is not an error, so prepare to reconnect as soon as requested. delayToConnect = DELAY_TO_RECONNECT_IN_MSECS socket?.disconnect() From ce7965d00e49da3cd48a1c6ff7650d3a3a89d36c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 12:51:08 -0400 Subject: [PATCH 008/185] Improves starting value of flows --- .../edits/PrivateStorageRelayListState.kt | 2 +- .../model/nip02FollowLists/FollowListState.kt | 2 +- .../topNavFeeds/FeedTopNavFilterState.kt | 3 +- .../model/topNavFeeds/IFeedFlowsType.kt | 2 + .../allFollows/AllFollowsFeedFlow.kt | 4 +- .../topNavFeeds/aroundMe/AroundMeFeedFlow.kt | 4 +- .../topNavFeeds/global/GlobalFeedFlow.kt | 4 +- .../topNavFeeds/noteBased/NoteFeedFlow.kt | 66 ++++++++++++++++--- .../topNavFeeds/unknown/UnknownFeedFlow.kt | 5 +- .../nip01Core/FilterHomePostsByGeohashes.kt | 2 +- .../nip22Comments/FilterPostsByScopes.kt | 2 +- .../FilterHomePostsByAllFollows.kt | 20 +++--- .../HomeOutboxEventsEoseManager.kt | 3 +- .../quartz/nip51Lists/MuteListEvent.kt | 12 ++-- .../quartz/nip51Lists/PeopleListEvent.kt | 6 +- .../nip51Lists/interests/HashtagListEvent.kt | 2 + .../nip51Lists/locations/GeohashListEvent.kt | 2 + .../follow/CommunityListEvent.kt | 2 + 18 files changed, 101 insertions(+), 42 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt index 4cf071b460..adec4d59bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt @@ -68,7 +68,7 @@ class PrivateStorageRelayListState( .stateIn( scope, SharingStarted.Eagerly, - emptySet(), + normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote()), ) fun saveRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt index 536faa18f7..87eca3f752 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt @@ -62,7 +62,7 @@ class FollowListState( @OptIn(ExperimentalCoroutinesApi::class) private val innerFlow: Flow = getFollowListFlow().transformLatest { - emit(buildKind3Follows(it.user.latestContactList)) + emit(buildKind3Follows(it.user.latestContactList ?: settings.backupContactList)) } val flow = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index b4dea3cf8f..ed0b9bccd9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsFeedFlo import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.AroundMeFeedFlow import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalFeedFlow import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.NoteFeedFlow -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.unknown.UnknownFeedFlow import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -80,6 +79,6 @@ class FeedTopNavFilterState( .stateIn( scope, SharingStarted.Eagerly, - AuthorsByOutboxTopNavFilter(emptySet()), + loadFlowsFor(feedFilterListName.value).startValue(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt index 5dc1a6b457..150fa4c992 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/IFeedFlowsType.kt @@ -26,5 +26,7 @@ import kotlinx.coroutines.flow.FlowCollector interface IFeedFlowsType { fun flow(): Flow + fun startValue(): IFeedTopNavFilter + suspend fun startValue(collector: FlowCollector) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt index 8052cb2767..e443909f54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt @@ -50,7 +50,9 @@ class AllFollowsFeedFlow( override fun flow() = allFollows.map(::convert) + override fun startValue(): AllFollowsByOutboxTopNavFilter = convert(allFollows.value) + override suspend fun startValue(collector: FlowCollector) { - collector.emit(convert(allFollows.value)) + collector.emit(startValue()) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt index 28db5545f3..22ceaa083e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt @@ -49,7 +49,9 @@ class AroundMeFeedFlow( override fun flow() = location.map(::convert) + override fun startValue(): LocationTopNavFilter = convert(location.value) + override suspend fun startValue(collector: FlowCollector) { - collector.emit(convert(location.value)) + collector.emit(startValue()) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt index 0a2141df27..ff6301ac19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt @@ -34,7 +34,9 @@ class GlobalFeedFlow( override fun flow() = MutableStateFlow(default) + override fun startValue(): GlobalTopNavFilter = default + override suspend fun startValue(collector: FlowCollector) { - collector.emit(default) + collector.emit(startValue()) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt index b30f723581..71104c81ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt @@ -49,17 +49,51 @@ class NoteFeedFlow( val signer: NostrSigner, val allFollowRelays: StateFlow>, ) : IFeedFlowsType { + fun process(noteEvent: Event): IFeedTopNavFilter = + when (noteEvent) { + is PeopleListEvent -> { + if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { + MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users) + } else { + AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users) + } + } + is MuteListEvent -> { + MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users) + } + is FollowListEvent -> { + AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet()) + } + is CommunityListEvent -> { + AllCommunitiesTopNavFilter(noteEvent.publicAndCachedPrivateCommunityIds().toSet()) + } + is HashtagListEvent -> { + HashtagTopNavFilter(noteEvent.publicAndCachedPrivateHashtags(), allFollowRelays) + } + is GeohashListEvent -> { + LocationTopNavFilter(noteEvent.publicAndCachedPrivateGeohash(), allFollowRelays) + } + is CommunityDefinitionEvent -> { + SingleCommunityTopNavFilter( + community = noteEvent.addressTag(), + authors = noteEvent.moderatorKeys().toSet().ifEmpty { null }, + relays = noteEvent.relayUrls().toSet(), + ) + } + else -> AuthorsByOutboxTopNavFilter(emptySet()) + } + suspend fun FlowCollector.process(noteEvent: Event) { when (noteEvent) { is PeopleListEvent -> { if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { - emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicUsersAndWords().users)) + emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users)) noteEvent.publicAndPrivateUsersAndWords(signer)?.let { emit(MutedAuthorsByOutboxTopNavFilter(it.users)) } } else { - emit(AuthorsByOutboxTopNavFilter(noteEvent.publicUsersAndWords().users)) + emit(AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users)) noteEvent.publicAndPrivateUsersAndWords(signer)?.let { emit(AuthorsByOutboxTopNavFilter(it.users)) @@ -67,7 +101,7 @@ class NoteFeedFlow( } } is MuteListEvent -> { - emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicUsersAndWords().users)) + emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users)) noteEvent.publicAndPrivateUsersAndWords(signer)?.let { emit(MutedAuthorsByOutboxTopNavFilter(it.users)) @@ -99,13 +133,18 @@ class NoteFeedFlow( } } is CommunityDefinitionEvent -> { - SingleCommunityTopNavFilter( - community = noteEvent.addressTag(), - authors = noteEvent.moderatorKeys().toSet().ifEmpty { null }, - relays = noteEvent.relayUrls().toSet(), + emit( + SingleCommunityTopNavFilter( + community = noteEvent.addressTag(), + authors = noteEvent.moderatorKeys().toSet().ifEmpty { null }, + relays = noteEvent.relayUrls().toSet(), + ), ) } - else -> AuthorsByOutboxTopNavFilter(emptySet()) + else -> + emit( + AuthorsByOutboxTopNavFilter(emptySet()), + ) } } @@ -120,10 +159,19 @@ class NoteFeedFlow( } } + override fun startValue(): IFeedTopNavFilter { + val noteEvent = metadataFlow.value?.note?.event + if (noteEvent == null) { + return AuthorsByOutboxTopNavFilter(emptySet()) + } else { + return process(noteEvent) + } + } + override suspend fun startValue(collector: FlowCollector) { val noteEvent = metadataFlow.value?.note?.event if (noteEvent == null) { - AuthorsByOutboxTopNavFilter(emptySet()) + collector.emit(AuthorsByOutboxTopNavFilter(emptySet())) } else { collector.process(noteEvent) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt index 4596d30986..075f449ca8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/unknown/UnknownFeedFlow.kt @@ -30,8 +30,11 @@ class UnknownFeedFlow( ) : IFeedFlowsType { override fun flow() = MutableStateFlow(UnknownTopNavFilter(feedName)) + // empty feed + override fun startValue(): UnknownTopNavFilter = UnknownTopNavFilter(feedName) + // empty feed override suspend fun startValue(collector: FlowCollector) { - collector.emit(UnknownTopNavFilter(feedName)) + collector.emit(startValue()) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt index 12b90b80a5..e6b332fcea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt @@ -56,7 +56,7 @@ val HomePostsByGeohashKinds = fun filterHomePostsByGeohashes( relay: NormalizedRelayUrl, geotags: Set, - since: Long, + since: Long?, ): List { if (geotags.isEmpty()) return emptyList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt index 04c29fb0fe..04172ee0db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt @@ -30,7 +30,7 @@ val CommentKinds = listOf(CommentEvent.KIND) fun filterHomePostsByScopes( relay: NormalizedRelayUrl, scopesToLoad: Set, - since: Long, + since: Long?, ): List { if (scopesToLoad.isEmpty()) return emptyList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt index dd1f0a0212..695b70236d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.f import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsFromAllCommunities import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterHomePostsByAllFollows( @@ -36,29 +35,26 @@ fun filterHomePostsByAllFollows( ): List { if (followsSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - - return followsSet.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince - val relay = it.key + return followsSet.set.flatMap { (relay, filter) -> + val since = since?.get(relay)?.time listOfNotNull( - it.value.authors?.let { + filter.authors?.let { filterHomePostsByAuthors(relay, it, since) }, - it.value.geotags?.let { + filter.geotags?.let { filterHomePostsByGeohashes(relay, it, since) }, - it.value.geotagScopes?.let { + filter.geotagScopes?.let { filterHomePostsByScopes(relay, it, since) }, - it.value.hashtags?.let { + filter.hashtags?.let { filterHomePostsByHashtags(relay, it, since) }, - it.value.hashtagScopes?.let { + filter.hashtagScopes?.let { filterHomePostsByScopes(relay, it, since) }, - it.value.communities?.let { + filter.communities?.let { filterHomePostsFromAllCommunities(relay, it, since) }, ).flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt index bbe992d11e..136751a4dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt @@ -44,7 +44,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class HomeOutboxEventsEoseManager( @@ -95,7 +94,7 @@ class HomeOutboxEventsEoseManager( } }, key.scope.launch(Dispatchers.Default) { - key.followRelayFlow().sample(2000).collectLatest { + key.followRelayFlow().collectLatest { invalidateFilters() } }, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt index 16719a9722..fc0eb9f1a1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt @@ -41,19 +41,19 @@ class MuteListEvent( ) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { override fun dTag() = FIXED_D_TAG - fun publicUsersAndWords() = - PeopleListEvent.UsersAndWords( - filterTagList("p", tags), - filterTagList("word", tags), + fun publicAndCachedUsersAndWords() = + UsersAndWords( + filterTagList("p", cachedPrivateTags()), + filterTagList("word", cachedPrivateTags()), ) fun publicAndPrivateUsersAndWords( signer: NostrSigner, - onReady: (PeopleListEvent.UsersAndWords) -> Unit, + onReady: (UsersAndWords) -> Unit, ) { privateTagsOrEmpty(signer) { onReady( - PeopleListEvent.UsersAndWords( + UsersAndWords( filterTagList("p", it), filterTagList("word", it), ), diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt index 08a80ad1a6..762660a8e2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt @@ -44,10 +44,10 @@ class PeopleListEvent( val words: Set = setOf(), ) - fun publicUsersAndWords() = + fun publicAndCachedPrivateUsersAndWords() = UsersAndWords( - filterTagList("p", tags), - filterTagList("word", tags), + filterTagList("p", cachedPrivateTags()), + filterTagList("word", cachedPrivateTags()), ) fun publicAndPrivateUsersAndWords( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt index d56059e18c..3e85f0797b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt @@ -46,6 +46,8 @@ class HashtagListEvent( fun publicHashtags() = tags.mapNotNull(HashtagTag::parse) + fun publicAndCachedPrivateHashtags() = publicHashtags().toSet() + (publicAndPrivateHashtagCache ?: emptySet()) + fun publicAndPrivateHashtag( signer: NostrSigner, onReady: (Set) -> Unit, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt index dff171129f..e1632c8a3a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt @@ -47,6 +47,8 @@ class GeohashListEvent( fun publicGeohashes() = tags.mapNotNull(GeoHashTag::parse) + fun publicAndCachedPrivateGeohash() = publicGeohashes().toSet() + (publicAndPrivateGeohashCache ?: emptySet()) + fun publicAndPrivateGeohash( signer: NostrSigner, onReady: (Set) -> Unit, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt index 2d5acf30d6..9f95d4dc5f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt @@ -54,6 +54,8 @@ class CommunityListEvent( fun publicCommunityIds() = tags.mapNotNull(ATag::parseAddressId) + fun publicAndCachedPrivateCommunityIds() = publicCommunityIds() + (publicAndPrivateAddressCache?.map { it.addressId } ?: emptyList()) + fun publicAndPrivateCommunities( signer: NostrSigner, onReady: (Set) -> Unit, From dd5c2ca704a85c391400371fd11286f9be97592c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 13:35:01 -0400 Subject: [PATCH 009/185] Making sure the order of params is right --- .../model/torState/TorRelaySettings.kt | 2 +- .../amethyst/model/torState/TorRelayState.kt | 48 ++++++++++++------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt index 77a59f42ce..e33477d052 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelaySettings.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.model.torState import com.vitorpamplona.amethyst.ui.tor.TorType -class TorRelaySettings( +data class TorRelaySettings( val torType: TorType = TorType.OFF, val onionRelaysViaTor: Boolean = true, val dmRelaysViaTor: Boolean = false, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt index 3358e4699a..db68542b43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt @@ -54,15 +54,21 @@ class TorRelayState( trustedRelaysViaTor: Boolean, newRelaysViaTor: Boolean, -> - TorRelaySettings(torType, onionRelaysViaTor, dmRelaysViaTor, trustedRelaysViaTor, newRelaysViaTor) + TorRelaySettings( + torType = torType, + onionRelaysViaTor = onionRelaysViaTor, + dmRelaysViaTor = dmRelaysViaTor, + trustedRelaysViaTor = trustedRelaysViaTor, + newRelaysViaTor = newRelaysViaTor, + ) }.onStart { emit( TorRelaySettings( - settings.torSettings.torType.value, - settings.torSettings.onionRelaysViaTor.value, - settings.torSettings.dmRelaysViaTor.value, - settings.torSettings.trustedRelaysViaTor.value, - settings.torSettings.newRelaysViaTor.value, + torType = settings.torSettings.torType.value, + onionRelaysViaTor = settings.torSettings.onionRelaysViaTor.value, + dmRelaysViaTor = settings.torSettings.dmRelaysViaTor.value, + trustedRelaysViaTor = settings.torSettings.trustedRelaysViaTor.value, + newRelaysViaTor = settings.torSettings.newRelaysViaTor.value, ), ) }.flowOn(Dispatchers.Default) @@ -70,11 +76,11 @@ class TorRelayState( scope, SharingStarted.Eagerly, TorRelaySettings( - settings.torSettings.torType.value, - settings.torSettings.onionRelaysViaTor.value, - settings.torSettings.dmRelaysViaTor.value, - settings.torSettings.trustedRelaysViaTor.value, - settings.torSettings.newRelaysViaTor.value, + torType = settings.torSettings.torType.value, + onionRelaysViaTor = settings.torSettings.onionRelaysViaTor.value, + dmRelaysViaTor = settings.torSettings.dmRelaysViaTor.value, + trustedRelaysViaTor = settings.torSettings.trustedRelaysViaTor.value, + newRelaysViaTor = settings.torSettings.newRelaysViaTor.value, ), ) @@ -84,13 +90,19 @@ class TorRelayState( trustedRelayState.flow, dmRelayState.flow, ) { torSettings: TorRelaySettings, trustedRelayList: Set, dmRelayList: Set -> - emit(TorRelayEvaluation(torSettings, trustedRelayList, dmRelayList)) + emit( + TorRelayEvaluation( + torSettings = torSettings, + trustedRelayList = trustedRelayList, + dmRelayList = dmRelayList, + ), + ) }.onStart { emit( TorRelayEvaluation( - torSettings.value, - trustedRelayState.flow.value, - dmRelayState.flow.value, + torSettings = torSettings.value, + trustedRelayList = trustedRelayState.flow.value, + dmRelayList = dmRelayState.flow.value, ), ) }.flowOn(Dispatchers.Default) @@ -98,9 +110,9 @@ class TorRelayState( scope, SharingStarted.Eagerly, TorRelayEvaluation( - torSettings.value, - trustedRelayState.flow.value, - dmRelayState.flow.value, + torSettings = torSettings.value, + trustedRelayList = trustedRelayState.flow.value, + dmRelayList = dmRelayState.flow.value, ), ) } From 29b70a52d4faf8903ef0584e6f8f381fdebacd57 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 14:11:26 -0400 Subject: [PATCH 010/185] Improves logs --- .../nip01Core/relay/client/single/basic/BasicRelayClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 2194df618d..a476fc5761 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -258,7 +258,7 @@ open class BasicRelayClient( msg: OkMessage, onConnected: () -> Unit, ) { - Log.w("Relay", "${url.url} Relay on OK: ${msg.eventId} ${msg.success} ${msg.message}") + Log.w("Relay", "${url.url} onOK: ${msg.eventId} ${msg.success} ${msg.message}") // if this is the OK of an auth event, renew all subscriptions and resend all outgoing events. if (authResponseWatcher.containsKey(msg.eventId)) { From 83899bda5a0e7fe3fd32bae263b6c26e93e74d69 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 14:11:45 -0400 Subject: [PATCH 011/185] Improves RelayInformation document with an empty one --- .../service/Nip11RelayInfoRetriever.kt | 84 +++++++++++++------ 1 file changed, 60 insertions(+), 24 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt index 62c0d28bb8..5a23ad813a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service import android.util.Log import android.util.LruCache import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import com.vitorpamplona.quartz.utils.TimeUtils @@ -35,28 +36,61 @@ import okhttp3.Response import java.io.IOException object Nip11CachedRetriever { - open class RetrieveResult( - val time: Long, - ) - - class RetrieveResultError( - val error: Nip11Retriever.ErrorCode, - val msg: String? = null, - ) : RetrieveResult(TimeUtils.now()) - - class RetrieveResultSuccess( + sealed class RetrieveResult( val data: Nip11RelayInformation, - ) : RetrieveResult(TimeUtils.now()) + val time: Long, + ) { + class Error( + data: Nip11RelayInformation, + val error: Nip11Retriever.ErrorCode, + val msg: String? = null, + ) : RetrieveResult(data, TimeUtils.now()) - class RetrieveResultLoading : RetrieveResult(TimeUtils.now()) + class Success( + data: Nip11RelayInformation, + ) : RetrieveResult(data, TimeUtils.now()) - private val relayInformationDocumentCache = LruCache(100) + class Loading( + data: Nip11RelayInformation, + ) : RetrieveResult(data, TimeUtils.now()) + + class Empty( + data: Nip11RelayInformation, + ) : RetrieveResult(data, TimeUtils.now()) + } + + private val relayInformationEmptyCache = LruCache(1000) + private val relayInformationDocumentCache = LruCache(1000) private val retriever = Nip11Retriever() - fun getFromCache(relay: NormalizedRelayUrl): Nip11RelayInformation? { - val result = relayInformationDocumentCache.get(relay) ?: return null - if (result is RetrieveResultSuccess) return result.data - return null + fun getEmpty(relay: NormalizedRelayUrl): Nip11RelayInformation { + relayInformationEmptyCache.get(relay)?.let { return it } + + val info = + Nip11RelayInformation( + name = relay.displayUrl(), + icon = relay.toHttp() + "favicon.ico", + ) + + relayInformationEmptyCache.put(relay, info) + + return info + } + + fun getFromCache(relay: NormalizedRelayUrl): Nip11RelayInformation { + val result = relayInformationDocumentCache.get(relay) + + return when (result) { + is RetrieveResult.Success -> return result.data + is RetrieveResult.Error -> return result.data + is RetrieveResult.Empty -> return result.data + is RetrieveResult.Loading -> return result.data + else -> { + val empty = getEmpty(relay) + relayInformationDocumentCache.put(relay, RetrieveResult.Empty(empty)) + empty + } + } } suspend fun loadRelayInfo( @@ -65,23 +99,25 @@ object Nip11CachedRetriever { onInfo: (Nip11RelayInformation) -> Unit, onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit, ) { - checkNotInMainThread() val doc = relayInformationDocumentCache.get(relay) if (doc != null) { - if (doc is RetrieveResultSuccess) { + if (doc is RetrieveResult.Success) { onInfo(doc.data) - } else if (doc is RetrieveResultLoading) { + } else if (doc is RetrieveResult.Loading) { if (TimeUtils.now() - doc.time < TimeUtils.ONE_MINUTE) { // just wait. } else { retrieve(relay, okHttpClient, onInfo, onError) } - } else if (doc is RetrieveResultError) { + } else if (doc is RetrieveResult.Error) { if (TimeUtils.now() - doc.time < TimeUtils.ONE_HOUR) { onError(relay, doc.error, null) } else { retrieve(relay, okHttpClient, onInfo, onError) } + } else { + // Empty + retrieve(relay, okHttpClient, onInfo, onError) } } else { retrieve(relay, okHttpClient, onInfo, onError) @@ -94,16 +130,16 @@ object Nip11CachedRetriever { onInfo: (Nip11RelayInformation) -> Unit, onError: (NormalizedRelayUrl, Nip11Retriever.ErrorCode, String?) -> Unit, ) { - relayInformationDocumentCache.put(relay, RetrieveResultLoading()) + relayInformationDocumentCache.put(relay, RetrieveResult.Loading(getEmpty(relay))) retriever.loadRelayInfo( relay = relay, okHttpClient = okHttpClient, onInfo = { - relayInformationDocumentCache.put(relay, RetrieveResultSuccess(it)) + relayInformationDocumentCache.put(relay, RetrieveResult.Success(it)) onInfo(it) }, onError = { relay, code, errorMsg -> - relayInformationDocumentCache.put(relay, RetrieveResultError(code, errorMsg)) + relayInformationDocumentCache.put(relay, RetrieveResult.Error(getEmpty(relay), code, errorMsg)) onError(relay, code, errorMsg) }, ) From a1a2e86c946b365608bdedb291871a378e92754a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 14:12:24 -0400 Subject: [PATCH 012/185] Reports don't offer hints --- .../quartz/nip56Reports/ReportEvent.kt | 20 +------------------ 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt index c6876c9bbf..099ea723a7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt @@ -24,17 +24,8 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider -import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider -import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip56Reports.tags.DefaultReportTag @@ -52,16 +43,7 @@ class ReportEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig), - EventHintProvider, - AddressHintProvider, - PubKeyHintProvider { - override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) - - override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) - - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) - +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { @Transient private var defaultType: ReportType? = null From abd895480d84cbf7630d83f6cdd30f2de2346ddf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 14:13:09 -0400 Subject: [PATCH 013/185] adds local servers to the "local definition" of the Normalized Url --- .../quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt | 2 +- .../quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt index c74f03348e..094e6fb0e6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt @@ -43,4 +43,4 @@ fun NormalizedRelayUrl.toHttp() = fun NormalizedRelayUrl.isOnion() = url.endsWith(".onion/") -fun NormalizedRelayUrl.isLocalHost() = url.contains("127.0.0.1") || url.contains("localhost") +fun NormalizedRelayUrl.isLocalHost() = url.contains("127.0.0.1") || url.contains("localhost") || url.contains(".local:") diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index 8d3a84549e..c6aa1491df 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -27,7 +27,7 @@ val normalizedUrls = LruCache(5000) class RelayUrlNormalizer { companion object { - fun isLocalHost(url: String) = url.contains("127.0.0.1") || url.contains("localhost") + fun isLocalHost(url: String) = url.contains("127.0.0.1") || url.contains("localhost") || url.contains(".local:") fun isOnion(url: String) = url.endsWith(".onion") || url.endsWith(".onion/") From 8c11ae5c87cb87d66792791d35d0513a2cc67432 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 14:13:36 -0400 Subject: [PATCH 014/185] Avoids adding local relays to the hint index. --- .../vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt index d6d498525e..99fc4a1c13 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintIndexer.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.hints.bloom.BloomFilterMurMur3 import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import com.vitorpamplona.quartz.utils.LargeCache /** @@ -43,8 +44,10 @@ class HintIndexer { relay: NormalizedRelayUrl, bloom: BloomFilterMurMur3, ) { - relayDB.put(relay, relay) - bloom.add(id, relay.hashCode()) + if (!relay.isLocalHost()) { + relayDB.put(relay, relay) + bloom.add(id, relay.hashCode()) + } } private fun getHintsFor( From d0e66504e459cc1bdd3db6a4b4e67ba797e4a42f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 14:16:26 -0400 Subject: [PATCH 015/185] Improves Relay Icon flow --- .../amethyst/ui/note/RelayListRow.kt | 25 +++--- .../header/ShortEphemeralChatChannelHeader.kt | 35 ++++---- .../nip11RelayInfo/Nip11RelayInformation.kt | 86 +++++++++---------- 3 files changed, 72 insertions(+), 74 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 5317a458eb..5e5eb7d386 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -127,15 +127,13 @@ fun RenderRelay( var openRelayDialog by remember { mutableStateOf(false) } if (openRelayDialog) { - relayInfo?.let { - RelayInformationDialog( - onClose = { openRelayDialog = false }, - relayInfo = it, - relay = relay, - accountViewModel = accountViewModel, - nav = nav, - ) - } + RelayInformationDialog( + onClose = { openRelayDialog = false }, + relayInfo = relayInfo, + relay = relay, + accountViewModel = accountViewModel, + nav = nav, + ) } val clipboardManager = LocalClipboardManager.current @@ -150,11 +148,10 @@ fun RenderRelay( clipboardManager.setText(AnnotatedString(relay.url)) }, onClick = { + openRelayDialog = true accountViewModel.retrieveRelayDocument( relay, - onInfo = { - openRelayDialog = true - }, + onInfo = { }, onError = { url, errorCode, exceptionMessage -> accountViewModel.toastManager.toast( R.string.unable_to_download_relay_document, @@ -185,8 +182,8 @@ fun RenderRelay( contentAlignment = Alignment.Center, ) { RenderRelayIcon( - displayUrl = relay.displayUrl(), - iconUrl = relayInfo?.icon, + displayUrl = relayInfo.id ?: relay.url, + iconUrl = relayInfo.icon, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, pingInMs = 0, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt index 6550a5f6df..795636635e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -29,12 +30,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.fasterxml.jackson.databind.util.ClassUtil.exceptionMessage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.FeatureSetType @@ -43,7 +46,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observe import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.produceStateIfNotNull import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.actions.JoinChatButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.actions.LeaveChatButton @@ -57,22 +59,21 @@ import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation fun loadRelayInfo( relay: NormalizedRelayUrl, accountViewModel: AccountViewModel, -): State { - @Suppress("ProduceStateDoesNotAssignValue") - val relayInfo = - produceStateIfNotNull( - Nip11CachedRetriever.getFromCache(relay), - relay, - ) { - accountViewModel.retrieveRelayDocument( - relay = relay, - onInfo = { value = it }, - onError = { url, errorCode, exceptionMessage -> }, - ) - } - - return relayInfo -} +): State = + produceState( + Nip11CachedRetriever.getFromCache(relay), + relay, + ) { + accountViewModel.retrieveRelayDocument( + relay = relay, + onInfo = { + value = it + }, + onError = { url, errorCode, exceptionMessage -> + Log.e("RelayInfo", "Error loading relay info for ${url.url}: $errorCode - $exceptionMessage") + }, + ) + } @Composable fun ShortEphemeralChatChannelHeader( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt index 4aded0889f..9506c16283 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt @@ -26,25 +26,25 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @Stable class Nip11RelayInformation( - val id: String?, - val name: String?, - val description: String?, - val icon: String?, - val pubkey: String?, - val contact: String?, - val supported_nips: List?, - val supported_nip_extensions: List?, - val software: String?, - val version: String?, - val limitation: RelayInformationLimitation?, - val relay_countries: List?, - val language_tags: List?, - val tags: List?, - val posting_policy: String?, - val payments_url: String?, - val retention: List?, - val fees: RelayInformationFees?, - val nip50: List?, + val id: String? = null, + val name: String? = null, + val description: String? = null, + val icon: String? = null, + val pubkey: String? = null, + val contact: String? = null, + val supported_nips: List? = null, + val supported_nip_extensions: List? = null, + val software: String? = null, + val version: String? = null, + val limitation: RelayInformationLimitation? = null, + val relay_countries: List? = null, + val language_tags: List? = null, + val tags: List? = null, + val posting_policy: String? = null, + val payments_url: String? = null, + val retention: List? = null, + val fees: RelayInformationFees? = null, + val nip50: List? = null, ) { companion object { val mapper = @@ -56,37 +56,37 @@ class Nip11RelayInformation( @Stable class RelayInformationFee( - val amount: Int?, - val unit: String?, - val period: Int?, - val kinds: List?, + val amount: Int? = null, + val unit: String? = null, + val period: Int? = null, + val kinds: List? = null, ) class RelayInformationFees( - val admission: List?, - val subscription: List?, - val publication: List?, + val admission: List? = null, + val subscription: List? = null, + val publication: List? = null, ) class RelayInformationLimitation( - val max_message_length: Int?, - val max_subscriptions: Int?, - val max_filters: Int?, - val max_limit: Int?, - val max_subid_length: Int?, - val min_prefix: Int?, - val max_event_tags: Int?, - val max_content_length: Int?, - val min_pow_difficulty: Int?, - val auth_required: Boolean?, - val payment_required: Boolean?, - val restricted_writes: Boolean?, - val created_at_lower_limit: Int?, - val created_at_upper_limit: Int?, + val max_message_length: Int? = null, + val max_subscriptions: Int? = null, + val max_filters: Int? = null, + val max_limit: Int? = null, + val max_subid_length: Int? = null, + val min_prefix: Int? = null, + val max_event_tags: Int? = null, + val max_content_length: Int? = null, + val min_pow_difficulty: Int? = null, + val auth_required: Boolean? = null, + val payment_required: Boolean? = null, + val restricted_writes: Boolean? = null, + val created_at_lower_limit: Int? = null, + val created_at_upper_limit: Int? = null, ) class RelayInformationRetentionData( - val kinds: ArrayList?, - val tiem: Int?, - val count: Int?, + val kinds: ArrayList? = null, + val tiem: Int? = null, + val count: Int? = null, ) From 67dc396588ab3404955942dce00b06bf99969136 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 14:52:03 -0400 Subject: [PATCH 016/185] Displays all available and connected relays in the relay screen --- .../loggedIn/relays/AllRelayListScreen.kt | 15 ++++ .../connected/ConnectedRelayListView.kt | 71 +++++++++++++++++++ .../connected/ConnectedRelayListViewModel.kt | 35 +++++++++ amethyst/src/main/res/values/strings.xml | 5 ++ 4 files changed, 126 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 5368a29087..68ae265cc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -55,6 +55,8 @@ import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.renderDMItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.LocalRelayListViewModel @@ -107,12 +109,16 @@ fun MappedAllRelayListView( val localViewModel: LocalRelayListViewModel = viewModel() val localFeedState by localViewModel.relays.collectAsStateWithLifecycle() + val connectedViewModel: ConnectedRelayListViewModel = viewModel() + val connectedRelays by connectedViewModel.relays.collectAsStateWithLifecycle() + LaunchedEffect(Unit) { dmViewModel.load(accountViewModel.account) nip65ViewModel.load(accountViewModel.account) searchViewModel.load(accountViewModel.account) localViewModel.load(accountViewModel.account) privateOutboxViewModel.load(accountViewModel.account) + connectedViewModel.load(accountViewModel.account) } Scaffold( @@ -241,6 +247,15 @@ fun MappedAllRelayListView( ) } renderLocalItems(localFeedState, localViewModel, accountViewModel, newNav) + + item { + SettingsCategory( + stringRes(R.string.connected_section), + stringRes(R.string.connected_section_description), + SettingsCategorySpacingModifier, + ) + } + renderConnectedItems(connectedRelays, connectedViewModel, accountViewModel, newNav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt new file mode 100644 index 0000000000..300883116d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.theme.FeedPadding + +@Composable +fun ConnectedRelayList( + postViewModel: ConnectedRelayListViewModel, + accountViewModel: AccountViewModel, + onClose: () -> Unit, + nav: INav, +) { + val newNav = rememberExtendedNav(nav, onClose) + val feedState by postViewModel.relays.collectAsStateWithLifecycle() + + Row(verticalAlignment = Alignment.CenterVertically) { + LazyColumn( + contentPadding = FeedPadding, + ) { + renderConnectedItems(feedState, postViewModel, accountViewModel, newNav) + } + } +} + +fun LazyListScope.renderConnectedItems( + feedState: List, + postViewModel: ConnectedRelayListViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + itemsIndexed(feedState, key = { _, item -> "Connected" + item.relay }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { }, + accountViewModel = accountViewModel, + nav, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt new file mode 100644 index 0000000000..416b7a3797 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() { + override fun getRelayList(): List = + account.client + .relayStatusFlow() + .value.available + .sorted() + + override fun saveRelayList(urlList: List) { + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 2d9b6cdb22..5e407892bb 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1061,6 +1061,11 @@ Insert between 1–3 relays to store events no one else can see, like your Drafts and/or app settings. Ideally, these relays are either local or require authentication before downloading each user\'s content. General Relays Amethyst uses these relays to download posts for you. + + + Connected Relays + Current list of relays being used + Recommended Relays Add the following relays to your General Relays list in order to receive posts from the listed users. Search Relays From 11d9af41bd89b93f9b7d6d92ad6d60b1d7dd9ef9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 14:52:16 -0400 Subject: [PATCH 017/185] Normalizes the click to open relay info dialog --- .../ui/actions/RelaySelectionDialog.kt | 151 ++++++++++-------- .../feed/types/RenderCreateChannelNote.kt | 21 ++- .../common/BasicRelaySetupInfoDialog.kt | 20 +-- 3 files changed, 103 insertions(+), 89 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt index 4b27cf41f2..89639c7116 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt @@ -47,13 +47,16 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import com.google.common.collect.Multimaps.index import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.FeedPadding @@ -84,8 +87,6 @@ fun RelaySelectionDialog( accountViewModel: AccountViewModel, nav: INav, ) { - val context = LocalContext.current - var relays by remember { mutableStateOf( accountViewModel.account.client.connectedRelayList().map { @@ -99,18 +100,6 @@ fun RelaySelectionDialog( val hasSelectedRelay by remember { derivedStateOf { relays.any { it.isSelected } } } - var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) } - - relayInfo?.let { - RelayInformationDialog( - onClose = { relayInfo = null }, - relayInfo = it.relayInfo, - relay = it.relay, - accountViewModel = accountViewModel, - nav = nav, - ) - } - var selected by remember { mutableStateOf(true) } Dialog( @@ -156,7 +145,7 @@ fun RelaySelectionDialog( Modifier.padding(pad).padding(start = 10.dp, end = 10.dp, top = 10.dp), ) { RelaySwitch( - text = stringRes(context, R.string.select_deselect_all), + text = stringRes(R.string.select_deselect_all), checked = selected, onClick = { selected = !selected @@ -171,9 +160,8 @@ fun RelaySelectionDialog( relays, key = { _, item -> item.relay }, ) { index, item -> - RelaySwitch( - text = item.relay.displayUrl(), - checked = item.isSelected, + RenderRelaySwitch( + item, onClick = { relays = relays.mapIndexed { j, item -> @@ -184,56 +172,8 @@ fun RelaySelectionDialog( } } }, - onLongPress = { - accountViewModel.retrieveRelayDocument( - relay = item.relay, - onInfo = { - relayInfo = - RelayInfoDialog( - item.relay, - it, - ) - }, - onError = { relay, errorCode, exceptionMessage -> - val msg = - when (errorCode) { - Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - } - - accountViewModel.toastManager.toast( - stringRes(context, R.string.unable_to_download_relay_document), - msg, - ) - }, - ) - }, + accountViewModel, + nav, ) } } @@ -242,6 +182,81 @@ fun RelaySelectionDialog( } } +@Composable +fun RenderRelaySwitch( + item: RelayList, + onClick: () -> Unit, + accountViewModel: AccountViewModel, + nav: INav, +) { + val context = LocalContext.current + + val relayInfo by loadRelayInfo(item.relay, accountViewModel) + + var openRelayDialog by remember { mutableStateOf(false) } + + if (openRelayDialog) { + RelayInformationDialog( + onClose = { openRelayDialog = false }, + relayInfo = relayInfo, + relay = item.relay, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + RelaySwitch( + text = item.relay.displayUrl(), + checked = item.isSelected, + onClick = onClick, + onLongPress = { + openRelayDialog = true + accountViewModel.retrieveRelayDocument( + relay = item.relay, + onInfo = { }, + onError = { relay, errorCode, exceptionMessage -> + val msg = + when (errorCode) { + Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> + stringRes( + context, + R.string.relay_information_document_error_assemble_url, + relay.url, + exceptionMessage, + ) + Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> + stringRes( + context, + R.string.relay_information_document_error_assemble_url, + relay.url, + exceptionMessage, + ) + Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> + stringRes( + context, + R.string.relay_information_document_error_assemble_url, + relay.url, + exceptionMessage, + ) + Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> + stringRes( + context, + R.string.relay_information_document_error_assemble_url, + relay.url, + exceptionMessage, + ) + } + + accountViewModel.toastManager.toast( + stringRes(context, R.string.unable_to_download_relay_document), + msg, + ) + }, + ) + }, + ) +} + @OptIn(ExperimentalFoundationApi::class) @Composable fun RelaySwitch( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt index aef777bed0..2890217475 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -244,15 +244,13 @@ fun RenderRelayLinePublicChat( var openRelayDialog by remember { mutableStateOf(false) } if (openRelayDialog) { - relayInfo?.let { - RelayInformationDialog( - onClose = { openRelayDialog = false }, - relayInfo = it, - relay = relay, - accountViewModel = accountViewModel, - nav = nav, - ) - } + RelayInformationDialog( + onClose = { openRelayDialog = false }, + relayInfo = relayInfo, + relay = relay, + accountViewModel = accountViewModel, + nav = nav, + ) } val clipboardManager = LocalClipboardManager.current @@ -263,11 +261,10 @@ fun RenderRelayLinePublicChat( clipboardManager.setText(AnnotatedString(relay.url)) }, onClick = { + openRelayDialog = true accountViewModel.retrieveRelayDocument( relay = relay, - onInfo = { - openRelayDialog = true - }, + onInfo = {}, onError = { relay, errorCode, exceptionMessage -> accountViewModel.toastManager.toast( R.string.unable_to_download_relay_document, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 465dcf5005..fe247aac03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -29,9 +29,9 @@ import androidx.compose.ui.platform.LocalContext import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.service.Nip11Retriever -import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog import com.vitorpamplona.amethyst.ui.stringRes @@ -42,14 +42,17 @@ fun BasicRelaySetupInfoDialog( accountViewModel: AccountViewModel, nav: INav, ) { - var relayInfo: RelayInfoDialog? by remember { mutableStateOf(null) } val context = LocalContext.current - relayInfo?.let { + val relayInfo by loadRelayInfo(item.relay, accountViewModel) + + var openRelayDialog by remember { mutableStateOf(false) } + + if (openRelayDialog) { RelayInformationDialog( - onClose = { relayInfo = null }, - relayInfo = it.relayInfo, - relay = it.relay, + onClose = { openRelayDialog = false }, + relayInfo = relayInfo, + relay = item.relay, accountViewModel = accountViewModel, nav = nav, ) @@ -62,11 +65,10 @@ fun BasicRelaySetupInfoDialog( onDelete = onDelete, accountViewModel = accountViewModel, onClick = { + openRelayDialog = true accountViewModel.retrieveRelayDocument( relay = item.relay, - onInfo = { - relayInfo = RelayInfoDialog(item.relay, it) - }, + onInfo = { }, onError = { relay, errorCode, exceptionMessage -> val msg = when (errorCode) { From e499410ddb443185faf6b0bdff835bc59035e1d0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 15:12:48 -0400 Subject: [PATCH 018/185] Fixes bug when the string has emojis and is trying to check if it is a hex or not --- quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt index 935942c912..270d66a5e0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/Hex.kt @@ -43,6 +43,7 @@ object Hex { try { for (c in hex.indices) { + if (c < 0 || c > 255) return false if (hexToByte[hex[c].code] < 0) return false } } catch (e: IllegalArgumentException) { From 3b260d98adb51d3bdd7b69465ffe94d51bdab086 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 15:15:19 -0400 Subject: [PATCH 019/185] Marks if it is using Tor in each relay of the list. --- .../relays/common/BasicRelaySetupInfo.kt | 11 ++++++++--- .../common/BasicRelaySetupInfoClickableRow.kt | 4 ++-- .../relays/common/BasicRelaySetupInfoModel.kt | 11 ++++++++--- .../relays/common/RelayNameAndRemoveButton.kt | 17 +++++++++++++++-- amethyst/src/main/res/values/strings.xml | 3 +++ 5 files changed, 36 insertions(+), 10 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt index c176d97507..3fac4efd4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfo.kt @@ -30,10 +30,15 @@ data class BasicRelaySetupInfo( val relay: NormalizedRelayUrl, val relayStat: RelayStat, val paidRelay: Boolean = false, + val forcesTor: Boolean = false, ) -fun relaySetupInfoBuilder(normalized: NormalizedRelayUrl): BasicRelaySetupInfo = +fun relaySetupInfoBuilder( + normalized: NormalizedRelayUrl, + forcesTor: Boolean = false, +): BasicRelaySetupInfo = BasicRelaySetupInfo( - normalized, - RelayStats.get(normalized), + relay = normalized, + relayStat = RelayStats.get(normalized), + forcesTor = forcesTor, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt index 6e54462ba0..596b9562a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt @@ -72,8 +72,8 @@ fun BasicRelaySetupInfoClickableRow( val iconUrlFromRelayInfoDoc by loadRelayInfo(item.relay, accountViewModel) RenderRelayIcon( - item.relay.displayUrl(), - iconUrlFromRelayInfoDoc?.icon, + iconUrlFromRelayInfoDoc.id ?: item.relay.displayUrl(), + iconUrlFromRelayInfoDoc.icon, loadProfilePicture, loadRobohash, item.relayStat.pingInMs, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index d53a68e268..5ae2115966 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -78,13 +78,18 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { } fun clear() { - var hasModified = false _relays.update { val relayList = getRelayList() ?: emptyList() relayList - .map { relaySetupInfoBuilder(it) } - .distinctBy { it.relay } + .map { + relaySetupInfoBuilder( + normalized = it, + forcesTor = + account.torRelayState.flow.value + .useTor(it), + ) + }.distinctBy { it.relay } .sortedBy { it.relayStat.receivedBytes } .reversed() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt index 183751a329..773a8877e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt @@ -36,6 +36,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -72,10 +73,22 @@ fun RelayNameAndRemoveButton( if (item.paidRelay) { Icon( imageVector = Icons.Default.Paid, - null, + contentDescription = stringRes(id = R.string.paid_relay), modifier = Modifier - .padding(start = 5.dp, top = 1.dp) + .padding(start = 5.dp) + .size(14.dp), + tint = MaterialTheme.colorScheme.allGoodColor, + ) + } + + if (item.forcesTor) { + Icon( + painter = painterResource(R.drawable.ic_tor), + contentDescription = stringRes(id = R.string.tor_relay), + modifier = + Modifier + .padding(start = 5.dp) .size(14.dp), tint = MaterialTheme.colorScheme.allGoodColor, ) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 5e407892bb..bac3065b01 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -244,6 +244,9 @@ Insert between 1–3 relays that host this group. Nostr clients use this setting to know where to download messages from and send your messages to. + Paid relay + Forces Tor when connecting + posts received Remove sats From 826718d688e3f2abb9ce1631319167752e0c8c5c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 15:35:47 -0400 Subject: [PATCH 020/185] Fixes author id on the outbox loader filter. --- .../model/topNavFeeds/OutboxRelayLoader.kt | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt index faa7fff9e9..7f784d533b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -38,14 +39,23 @@ class OutboxRelayLoader { ): Map> = mapOfSet { outboxRelayNotes.forEach { outboxNote -> - val relays = - (outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.ifEmpty { null } - ?: outboxNote.note.author - ?.pubkeyHex - ?.let { cache.relayHints.hintsForKey(it) } + val note = outboxNote.note - relays?.forEach { - add(it, outboxNote.note.idHex) + val authorHex = + if (note is AddressableNote) { + note.address.pubKeyHex + } else { + note.author?.pubkeyHex + } + + if (authorHex != null) { + val relays = + (outboxNote.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.ifEmpty { null } + ?: cache.relayHints.hintsForKey(authorHex) + + relays.forEach { + add(it, authorHex) + } } } } From 0b102df11775bab8d6e5ac6bff0f706badee1b1d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 15:50:50 -0400 Subject: [PATCH 021/185] Removes local relays from NIP65 lists. --- .../quartz/nip01Core/relay/filters/Filter.kt | 20 +++++++++++++++++++ .../relay/normalizer/NormalizedRelayUrl.kt | 4 ++-- .../relay/normalizer/RelayUrlNormalizer.kt | 9 +++++++-- .../quartz/nip17Dm/settings/tags/RelayTag.kt | 5 ++++- .../tags/AdvertisedRelayInfoTag.kt | 11 +++++++++- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index 9b7cf1a521..5472bb9c0d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.filters import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address class Filter( val ids: List? = null, @@ -46,4 +47,23 @@ class Filter( limit: Int? = this.limit, search: String? = this.search, ) = Filter(ids, authors, kinds, tags, since, until, limit, search) + + init { + // TODO: Remove this in production + ids?.forEach { + if (it.length != 64) throw IllegalArgumentException("Invalid id length $it on ${toJson()}") + } + authors?.forEach { + if (it.length != 64) throw IllegalArgumentException("Invalid author length $it on ${toJson()}") + } + tags?.get("#p")?.forEach { + if (it.length != 64) throw IllegalArgumentException("Invalid p-tag length $it on ${toJson()}") + } + tags?.get("#e")?.forEach { + if (it.length != 64) throw IllegalArgumentException("Invalid e-tag length $it on ${toJson()}") + } + tags?.get("#a")?.forEach { + if (Address.parse(it) != null) throw IllegalArgumentException("Invalid a-tag length $it on ${toJson()}") + } + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt index 094e6fb0e6..d9e80aefa4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/NormalizedRelayUrl.kt @@ -41,6 +41,6 @@ fun NormalizedRelayUrl.toHttp() = "https://$url" } -fun NormalizedRelayUrl.isOnion() = url.endsWith(".onion/") +fun NormalizedRelayUrl.isOnion() = url.contains(".onion/") -fun NormalizedRelayUrl.isLocalHost() = url.contains("127.0.0.1") || url.contains("localhost") || url.contains(".local:") +fun NormalizedRelayUrl.isLocalHost() = RelayUrlNormalizer.isLocalHost(this.url) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index c6aa1491df..453710a29b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -27,9 +27,14 @@ val normalizedUrls = LruCache(5000) class RelayUrlNormalizer { companion object { - fun isLocalHost(url: String) = url.contains("127.0.0.1") || url.contains("localhost") || url.contains(".local:") + fun isLocalHost(url: String) = + url.contains("127.0.0.1") || + url.contains("localhost") || + url.contains("//umbrel:") || + url.contains("192.168.") || + url.contains(".local:") - fun isOnion(url: String) = url.endsWith(".onion") || url.endsWith(".onion/") + fun isOnion(url: String) = url.endsWith(".onion") || url.contains(".onion/") private fun norm(url: String) = NormalizedRelayUrl( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/tags/RelayTag.kt index 19a2762cd9..d1c50cf20a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/tags/RelayTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/tags/RelayTag.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip17Dm.settings.tags import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import com.vitorpamplona.quartz.utils.ensure class RelayTag { @@ -41,7 +42,9 @@ class RelayTag { ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) + + ensure(relay != null && !relay.isLocalHost()) { return null } return relay } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt index 4bb8d237b3..9a7de3008b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import com.vitorpamplona.quartz.utils.ensure import kotlin.text.isBlank @@ -79,6 +80,10 @@ class AdvertisedRelayInfo( ensure(match(tag)) { return null } ensure(AdvertisedRelayType.isRead(tag.getOrNull(2))) { return null } + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) + + ensure(relay != null && !relay.isLocalHost()) { return null } + return RelayUrlNormalizer.normalizeOrNull(tag[1]) } @@ -87,7 +92,11 @@ class AdvertisedRelayInfo( ensure(match(tag)) { return null } ensure(AdvertisedRelayType.isWrite(tag.getOrNull(2))) { return null } - return RelayUrlNormalizer.normalizeOrNull(tag[1]) + val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) + + ensure(relay != null && !relay.isLocalHost()) { return null } + + return relay } @JvmStatic From 5525bc4bd9e421289e5d9816e8b46dafb4331dad Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 17:42:46 -0400 Subject: [PATCH 022/185] Increasing the thread pool for OkHttp in order to sustain more connections. --- .../service/okhttp/OkHttpClientFactory.kt | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index bd3e2aec89..769d2372ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -20,6 +20,13 @@ */ package com.vitorpamplona.amethyst.service.okhttp +import android.util.Log +import com.vitorpamplona.amethyst.isDebug +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import okhttp3.Dispatcher import okhttp3.OkHttpClient import java.net.InetSocketAddress import java.net.Proxy @@ -36,9 +43,29 @@ class OkHttpClientFactory( const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30 } + val logging = LoggingInterceptor() + val keyDecryptor = EncryptedBlobInterceptor(keyCache) + + val myDispatcher = + Dispatcher().apply { + maxRequests = 512 + } + + init { + if (isDebug) { + GlobalScope.launch(Dispatchers.IO) { + while (true) { + Log.d("OkHttpClientFactory", "Active threads ${myDispatcher.runningCallsCount()}") + delay(5000) + } + } + } + } + private val rootClient = OkHttpClient .Builder() + .dispatcher(myDispatcher) .followRedirects(true) .followSslRedirects(true) .build() @@ -57,8 +84,8 @@ class OkHttpClientFactory( .connectTimeout(duration) .writeTimeout(duration) .addInterceptor(DefaultContentTypeInterceptor(userAgent)) - .addNetworkInterceptor(LoggingInterceptor()) - .addNetworkInterceptor(EncryptedBlobInterceptor(keyCache)) + .addNetworkInterceptor(logging) + .addNetworkInterceptor(keyDecryptor) .build() } From ad481e1928e9d90ee1ca160cb5dc4e0eba20d433 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 17:59:14 -0400 Subject: [PATCH 023/185] Removes another local relay from the list --- .../quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index 453710a29b..401f6bf0ee 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -32,7 +32,8 @@ class RelayUrlNormalizer { url.contains("localhost") || url.contains("//umbrel:") || url.contains("192.168.") || - url.contains(".local:") + url.contains(".local:") || + url.contains(".local/") fun isOnion(url: String) = url.endsWith(".onion") || url.contains(".onion/") From 1fa2592563a3f44d8f7d9d2fd644e8a8dd730336 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 2 Jul 2025 19:36:33 -0400 Subject: [PATCH 024/185] Improves the performance and logging structures for the anti-spam filter. --- .../com/vitorpamplona/amethyst/DebugUtils.kt | 2 +- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/AntiSpamFilter.kt | 122 +++++++++++++----- .../nip01Core/relay/client/NostrClient.kt | 5 - 4 files changed, 93 insertions(+), 38 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 7caf3ef17b..71ed235f27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -112,7 +112,7 @@ fun debugState(context: Context) { Log.d( "STATE DUMP", "Spam: " + - LocalCache.antiSpam.spamMessages.size() + " / " + LocalCache.antiSpam.recentMessages.size(), + LocalCache.antiSpam.spamMessages.size() + " / " + LocalCache.antiSpam.recentEventIds.size() + " / " + LocalCache.antiSpam.recentAddressables.size(), ) Log.d( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index c430d92ac6..8a20110a82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2265,7 +2265,7 @@ class Account( scope.launch(Dispatchers.Default) { cache.antiSpam.flowSpam.collect { it.cache.spamMessages.snapshot().values.forEach { spammer -> - if (!hiddenUsers.isHidden(spammer.pubkeyHex) && spammer.duplicatedMessages.size >= 5) { + if (!hiddenUsers.isHidden(spammer.pubkeyHex) && spammer.shouldHide()) { if (spammer.pubkeyHex != userProfile().pubkeyHex && spammer.pubkeyHex !in followingKeySet()) { hiddenUsers.hideUser(spammer.pubkeyHex) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 731951eadc..4b2defdcb1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -22,75 +22,114 @@ package com.vitorpamplona.amethyst.model import android.util.Log import android.util.LruCache -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.njumpLink +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import kotlinx.coroutines.flow.MutableStateFlow data class Spammer( val pubkeyHex: HexKey, - var duplicatedMessages: Set, -) + var duplicatedEventIds: Set, + var duplicatedEventAddresses: Set
, +) { + fun shouldHide() = duplicatedEventIds.size >= 5 || duplicatedEventAddresses.size >= 5 +} class AntiSpamFilter { - val recentMessages = LruCache(1000) + val recentEventIds = LruCache(2000) + val recentAddressables = LruCache(2000) val spamMessages = LruCache(1000) var active: Boolean = true + fun spamHashCode(event: Event): Int = 31 * event.content.hashCode() + event.tags.contentDeepHashCode() + fun isSpam( event: Event, relay: NormalizedRelayUrl?, ): Boolean { - checkNotInMainThread() - if (!active) return false - val idHex = event.id - // if short message, ok // The idea here is to avoid considering repeated "GM" messages spam. - if (event.content.length < 50) return false + if (event.content.length < 60) return false // if the message is actually short but because it cites a user/event, the nostr: string is // really long, make it ok. // The idea here is to avoid considering repeated "@Bot, command" messages spam, while still // blocking repeated "lnbc..." invoices or fishing urls - if (event.content.length < 180 && Nip19Parser.nip19regex.matcher(event.content).find()) return false + if (event.content.length < 180 && event.content.startsWith("nostr:") && Nip19Parser.nip19regex.matcher(event.content).find()) return false // double list strategy: // if duplicated, it goes into spam. 1000 spam messages are saved into the spam list. // Considers tags so that same replies to different people don't count. - val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode() + val hash = spamHashCode(event) - if ( - (recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null - ) { - Log.w( - "Potential SPAM Message", - "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}", - ) + // ignores multiple versions of the same addressable. + if (event is AddressableEvent) { + val address = event.address() - // Log down offenders - logOffender(hash, event) + // normal event + if ( + (recentAddressables[hash] != null && recentAddressables[hash] != address) || + (spamMessages[hash] != null && !spamMessages[hash].duplicatedEventAddresses.contains(address)) + ) { + val existingAddress = recentAddressables[hash] - if (relay != null) { - RelayStats.newSpam(relay, njumpLink(NEvent.create(event.id, event.pubKey, event.kind, relay))) + val link1 = njumpLink(NAddress.create(existingAddress.kind, existingAddress.pubKeyHex, existingAddress.dTag, relay)) + val link2 = njumpLink(NAddress.create(event.kind, event.pubKey, event.dTag(), relay)) + + Log.w("Duplicated/SPAM", "${relay?.url} $link1 $link2") + + // Log down offenders + val spammer = logOffender(hash, event) + + if (spammer.shouldHide() && relay != null) { + RelayStats.newSpam(relay, "$link1 $link2") + } + + flowSpam.tryEmit(AntiSpamState(this)) + + return true } - flowSpam.tryEmit(AntiSpamState(this)) + recentAddressables.put(hash, address) + } else { + // normal event + if ( + (recentEventIds[hash] != null && recentEventIds[hash] != event.id) || + (spamMessages[hash] != null && !spamMessages[hash].duplicatedEventIds.contains(event.id)) + ) { + val existingEvent = recentEventIds[hash] - return true + val link1 = njumpLink(NEvent.create(existingEvent, null, null, relay)) + val link2 = njumpLink(NEvent.create(event.id, null, null, relay)) + + Log.w("Duplicated/SPAM", "${relay?.url} $link1 $link2") + + // Log down offenders + val spammer = logOffender(hash, event) + + if (spammer.shouldHide() && relay != null) { + RelayStats.newSpam(relay, "$link1 $link2") + } + + flowSpam.tryEmit(AntiSpamState(this)) + + return true + } + + recentEventIds.put(hash, event.id) } - recentMessages.put(hash, idHex) - return false } @@ -98,12 +137,33 @@ class AntiSpamFilter { private fun logOffender( hashCode: Int, event: Event, - ) { - if (spamMessages.get(hashCode) == null) { - spamMessages.put(hashCode, Spammer(event.pubKey, setOf(recentMessages[hashCode], event.id))) + ): Spammer { + val spammer = spamMessages.get(hashCode) + + if (spammer == null) { + val newSpammer = + if (event is AddressableEvent) { + Spammer( + pubkeyHex = event.pubKey, + duplicatedEventIds = setOf(), + duplicatedEventAddresses = setOf(recentAddressables[hashCode], event.address()), + ) + } else { + Spammer( + pubkeyHex = event.pubKey, + duplicatedEventIds = setOf(recentEventIds[hashCode], event.id), + duplicatedEventAddresses = setOf(), + ) + } + spamMessages.put(hashCode, newSpammer) + return newSpammer } else { - val spammer = spamMessages.get(hashCode) - spammer.duplicatedMessages += event.id + if (event is AddressableEvent) { + spammer.duplicatedEventAddresses += event.address() + } else { + spammer.duplicatedEventIds += event.id + } + return spammer } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index c9500f43bd..0c61ccc7a1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.client -import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState @@ -115,10 +114,6 @@ class NostrClient( subscriptionId: String = newSubId(), filters: List = listOf(), ) { - filters.forEach { - Log.d("NostrClient", "${it.relay.url} ${it.filter.toJson()}") - } - activeSubscriptions.addOrUpdate(subscriptionId, filters) relayPool.sendRequest(subscriptionId, filters) } From 5bcaf065a3b9a1eb743c2fbc50d57f91143af548 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 10:19:33 -0400 Subject: [PATCH 025/185] Fixing logs for the new list of connected relays. --- .../java/com/vitorpamplona/amethyst/DebugUtils.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 71ed235f27..3b3301f601 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -58,7 +58,16 @@ fun debugState(context: Context) { Log.d("STATE DUMP", "Memory Class $memClass MB (largeHeap $isLargeHeap)") } - Log.d("STATE DUMP", "Connected Relays: " + Amethyst.instance.client.connectedRelays()) + Log.d( + "STATE DUMP", + "Connected Relays: " + + Amethyst.instance.client + .relayStatusFlow() + .value.connected.size + "/" + + Amethyst.instance.client + .relayStatusFlow() + .value.available.size, + ) Log.d( "STATE DUMP", From 69db9bdcd5db5ea3a7d9168a5c10e8819fed5584 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 10:27:47 -0400 Subject: [PATCH 026/185] Reviews onDispose calls to make sure the onDispose is disposing the data from the previous composable, since it can happen AFTER a new composition already took place. So the references must match the old composition. --- .../composable/ControlWhenPlayerIsActive.kt | 34 ++++++++++------- .../playback/composable/GetVideoController.kt | 4 ++ .../playback/composable/wavefront/Waveform.kt | 26 +++++++------ .../service/playback/pip/BackgroundMedia.kt | 8 ++-- .../service/playback/pip/PictureInPicture.kt | 5 +-- .../service/playback/pip/PipVideoView.kt | 2 +- .../ui/actions/NewUserMetadataScreen.kt | 7 ---- .../amethyst/ui/navigation/AppBottomBar.kt | 1 - .../amethyst/ui/screen/AccountState.kt | 13 ++++--- .../ui/screen/loggedIn/LoggedInPage.kt | 29 +++++++------- .../loggedIn/profile/relays/TabRelays.kt | 2 - .../ui/screen/loggedOff/LoginScreen.kt | 38 ++++++++++--------- .../ExternalSignerLauncher.kt | 8 ++-- 13 files changed, 97 insertions(+), 80 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/ControlWhenPlayerIsActive.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/ControlWhenPlayerIsActive.kt index fa78b283d8..19fcc64821 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/ControlWhenPlayerIsActive.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/ControlWhenPlayerIsActive.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.playback.composable +import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -67,23 +68,30 @@ fun ControlWhenPlayerIsActive( // Keeps the screen on while playing and viewing videos. DisposableEffect(key1 = controller, key2 = view) { - val listener = - object : Player.Listener { - override fun onIsPlayingChanged(isPlaying: Boolean) { - // doesn't consider the mutex because the screen can turn off if the video - // being played in the mutex is not visible. - if (view.keepScreenOn != isPlaying) { - view.keepScreenOn = isPlaying - } - } - } + val listener = PlayerEventListener(view) controller.addListener(listener) onDispose { - if (view.keepScreenOn) { - view.keepScreenOn = false - } controller.removeListener(listener) + listener.destroy() + } + } +} + +class PlayerEventListener( + val view: View, +) : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + // doesn't consider the mutex because the screen can turn off if the video + // being played in the mutex is not visible. + if (view.keepScreenOn != isPlaying) { + view.keepScreenOn = isPlaying + } + } + + fun destroy() { + if (view.keepScreenOn) { + view.keepScreenOn = false } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt index 52850fd07e..f264bc9f53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/GetVideoController.kt @@ -54,6 +54,10 @@ fun GetVideoController( val scope = rememberCoroutineScope() // Prepares a VideoPlayer from the foreground service. + // + // TODO: Review this code because a new Disposable Effect can run + // before the onDispose of the previous composable and the onDispose + // sometimes affects the new variables, not the old ones. DisposableEffect(key1 = mediaItem.src.videoUri) { // If it is not null, the user might have come back from a playing video, like clicking on // the notification of the video player. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt index 57da6b3119..3307cbf99d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt @@ -56,21 +56,25 @@ fun Waveform( val restartFlow = remember { mutableIntStateOf(0) } + val myController = mediaControllerState.controller + // Keeps the screen on while playing and viewing videos. - DisposableEffect(key1 = mediaControllerState.controller) { - val listener = - object : Player.Listener { - override fun onIsPlayingChanged(isPlaying: Boolean) { - // doesn't consider the mutex because the screen can turn off if the video - // being played in the mutex is not visible. - if (isPlaying) { - restartFlow.intValue += 1 + if (myController != null) { + DisposableEffect(key1 = myController) { + val listener = + object : Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + // doesn't consider the mutex because the screen can turn off if the video + // being played in the mutex is not visible. + if (isPlaying) { + restartFlow.intValue += 1 + } } } - } - mediaControllerState.controller?.addListener(listener) - onDispose { mediaControllerState.controller?.removeListener(listener) } + myController.addListener(listener) + onDispose { myController.removeListener(listener) } + } } LaunchedEffect(key1 = restartFlow.intValue) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/BackgroundMedia.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/BackgroundMedia.kt index edddaf1ac1..1649d665e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/BackgroundMedia.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/BackgroundMedia.kt @@ -39,7 +39,7 @@ object BackgroundMedia { fun removeBackgroundControllerAndReleaseIt() { bgInstance.value?.let { PlaybackServiceClient.removeController(it) - clearBackground() + bgInstance.tryEmit(null) } } @@ -47,7 +47,9 @@ object BackgroundMedia { bgInstance.tryEmit(mediaControllerState) } - fun clearBackground() { - bgInstance.tryEmit(null) + fun clearBackground(mediaControllerState: MediaControllerState) { + if (bgInstance.value == mediaControllerState) { + bgInstance.tryEmit(null) + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PictureInPicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PictureInPicture.kt index 42ebcff9ee..c2a2008b66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PictureInPicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PictureInPicture.kt @@ -58,9 +58,8 @@ fun rememberIsInPipMode(): Boolean { Consumer { info -> pipMode = info.isInPictureInPictureMode } - activity.addOnPictureInPictureModeChangedListener( - observer, - ) + + activity.addOnPictureInPictureModeChangedListener(observer) onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } } return pipMode diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt index 5a856db04c..168e03897e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/pip/PipVideoView.kt @@ -81,7 +81,7 @@ fun PipVideo(controller: MediaControllerState) { DisposableEffect(controller) { BackgroundMedia.switchKeepPlaying(controller) onDispose { - BackgroundMedia.clearBackground() + BackgroundMedia.clearBackground(controller) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt index 26af4a4ce0..7338a622c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt @@ -40,7 +40,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -74,12 +73,6 @@ fun NewUserMetadataScreen( postViewModel.load(accountViewModel.account) } - DisposableEffect(Unit) { - onDispose { - postViewModel.clear() - } - } - Scaffold( topBar = { TopAppBar( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index c244e81161..dd57aefdcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -104,7 +104,6 @@ fun keyboardAsState(): State { } } view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) - onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt index 2d07e3c0a9..87321b417c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner @@ -55,11 +54,13 @@ fun SetAccountCentricViewModelStore( content() } - DisposableEffect(key1 = state) { - onDispose { - state.currentViewModelStore.viewModelStore.clear() - } - } + // moved this clearing activity to the viewmodel account + // because the new composable might run before the onDispose. + // DisposableEffect(key1 = state) { + // onDispose { + // state.currentViewModelStore.viewModelStore.clear() + // } + // } } class AccountCentricViewModelStore : ViewModelStoreOwner { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index 65df1f06f8..0c46ae6b91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.app.Activity +import android.content.Intent import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -216,25 +217,27 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) { } } + val launcher: (Intent) -> Unit = { + try { + launcher.launch(it) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("Signer", "Error opening Signer app", e) + accountViewModel.toastManager.toast( + R.string.error_opening_external_signer, + R.string.error_opening_external_signer_description, + ) + } + } + lifeCycleOwner.lifecycle.addObserver(observer) accountViewModel.account.signer.launcher.registerLauncher( - launcher = { - try { - launcher.launch(it) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("Signer", "Error opening Signer app", e) - accountViewModel.toastManager.toast( - R.string.error_opening_external_signer, - R.string.error_opening_external_signer_description, - ) - } - }, + launcher = launcher, contentResolver = Amethyst.instance::contentResolverFn, ) onDispose { accountViewModel.account.signer.launcher - .clearLauncher() + .clearLauncherIf(launcher) lifeCycleOwner.lifecycle.removeObserver(observer) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt index e55e66e2b1..5e5c4864c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt @@ -64,8 +64,6 @@ fun TabRelays( lifeCycleOwner.lifecycle.addObserver(observer) onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) - println("Profile Relay Dispose") - feedViewModel.unsubscribeTo(user) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index ac86217662..a46c60fa67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -641,26 +641,30 @@ private fun PrepareExternalSignerReceiver(onLogin: (pubkey: String, packageName: val activity = getActivity() as MainActivity DisposableEffect(launcher, activity, externalSignerLauncher) { - externalSignerLauncher.registerLauncher( - launcher = { - try { - launcher.launch(it) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("Signer", "Error opening Signer app", e) - scope.launch(Dispatchers.Main) { - Toast - .makeText( - Amethyst.instance, - R.string.error_opening_external_signer, - Toast.LENGTH_SHORT, - ).show() - } + val launcher: (Intent) -> Unit = { + try { + launcher.launch(it) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("Signer", "Error opening Signer app", e) + scope.launch(Dispatchers.Main) { + Toast + .makeText( + Amethyst.instance, + R.string.error_opening_external_signer, + Toast.LENGTH_SHORT, + ).show() } - }, + } + } + + externalSignerLauncher.registerLauncher( + launcher = launcher, contentResolver = Amethyst.instance::contentResolverFn, ) - onDispose { externalSignerLauncher.clearLauncher() } + onDispose { + externalSignerLauncher.clearLauncherIf(launcher) + } } LaunchedEffect(externalSignerLauncher) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt index 2a5a7507c1..ff7c5e4c4f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt @@ -114,9 +114,11 @@ class ExternalSignerLauncher( } /** Call this function when the activity is destroyed or is about to be replaced. */ - fun clearLauncher() { - this.signerAppLauncher = null - this.contentResolver = null + fun clearLauncherIf(launcher: ((Intent) -> Unit)) { + if (signerAppLauncher == launcher) { + this.signerAppLauncher = null + this.contentResolver = null + } } fun newResult(data: Intent) { From 1f4ad32d231e0b826262928ec381c27d990b5ca4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 10:29:17 -0400 Subject: [PATCH 027/185] Fixes the clearing of the mapping function between users and subscription ids when subscriptions end --- .../eoseManagers/PerUniqueIdEoseManager.kt | 7 +++---- .../eoseManagers/PerUserAndFollowListEoseManager.kt | 11 +++++------ .../relayClient/eoseManagers/PerUserEoseManager.kt | 7 +++---- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt index b4ce748ef5..8cf1b6df9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -70,6 +70,7 @@ abstract class PerUniqueIdEoseManager( subId: String, ) { orchestrator.dismissSubscription(subId) + userSubscriptionMap.remove(key) } fun findOrCreateSubFor(key: T): Subscription { @@ -94,10 +95,8 @@ abstract class PerUniqueIdEoseManager( updated.add(mainKey) } - userSubscriptionMap.forEach { - if (it.key !in updated) { - endSub(it.key, it.value) - } + userSubscriptionMap.filter { it.key !in updated }.forEach { + endSub(it.key, it.value) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index 62e130b8b3..0c0da81ec8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -68,6 +68,7 @@ abstract class PerUserAndFollowListEoseManager( subId: String, ) { orchestrator.dismissSubscription(subId) + userSubscriptionMap.remove(key) } fun findOrCreateSubFor(key: T): Subscription { @@ -87,15 +88,13 @@ abstract class PerUserAndFollowListEoseManager( uniqueSubscribedAccounts.forEach { val user = user(it) - findOrCreateSubFor(it).relayBasedFilters = updateFilter(it, since(it))?.ifEmpty { null } - + val sub = findOrCreateSubFor(it) + sub.relayBasedFilters = updateFilter(it, since(it))?.ifEmpty { null } updated.add(user) } - userSubscriptionMap.forEach { - if (it.key !in updated) { - endSub(it.key, it.value) - } + userSubscriptionMap.filter { it.key !in updated }.forEach { + endSub(it.key, it.value) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index 8f06ae1ee7..1854f57d1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -67,6 +67,7 @@ abstract class PerUserEoseManager( subId: String, ) { orchestrator.dismissSubscription(subId) + userSubscriptionMap.remove(key) } fun findOrCreateSubFor(key: T): Subscription { @@ -91,10 +92,8 @@ abstract class PerUserEoseManager( updated.add(user) } - userSubscriptionMap.forEach { - if (it.key !in updated) { - endSub(it.key, it.value) - } + userSubscriptionMap.filter { it.key !in updated }.forEach { + endSub(it.key, it.value) } } From da3dbb0321376dda79ee3cdee7e4e461b14369dc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 10:30:19 -0400 Subject: [PATCH 028/185] Fixes the Account switching issue that was calling onDispose AFTER a new user composition was run and thus closing all connections on switching --- .../RelaySubscriptionsCoordinator.kt | 4 -- ...laySubscriptionsCoordinatorSubscription.kt | 38 ------------------- .../ui/screen/loggedIn/LoggedInPage.kt | 4 -- 3 files changed, 46 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinatorSubscription.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt index fb0c247746..4eeb50ed5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt @@ -98,10 +98,6 @@ class RelaySubscriptionsCoordinator( nwc, ) - fun start() = all.forEach { it.start() } - - fun stop() = all.forEach { it.stop() } - fun destroy() = all.forEach { it.destroy() } fun printCounters() = all.forEach { it.printStats() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinatorSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinatorSubscription.kt deleted file mode 100644 index f6aba1cb2e..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinatorSubscription.kt +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.service.relayClient.reqCommand - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel - -@Composable -fun RelaySubscriptionsCoordinatorSubscription(accountViewModel: AccountViewModel) = RelaySubscriptionsCoordinatorSubscription(accountViewModel.dataSources()) - -@Composable -fun RelaySubscriptionsCoordinatorSubscription(dataSource: RelaySubscriptionsCoordinator) { - DisposableEffect(Unit) { - dataSource.start() - onDispose { - dataSource.stop() - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index 0c46ae6b91..13ecb24c5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -43,7 +43,6 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthSubscription -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinatorSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.components.getActivity @@ -98,9 +97,6 @@ fun LoggedInPage( VideoFilterAssemblerSubscription(accountViewModel) DiscoveryFilterAssemblerSubscription(accountViewModel) - // TODO: Is this needed? - RelaySubscriptionsCoordinatorSubscription(accountViewModel) - // Updates local cache of the anti-spam filter choice of this user. ObserveAntiSpamFilterSettings(accountViewModel) From 47e69ce045ec8fcd7b3130983b267a2de44bec75 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 10:30:53 -0400 Subject: [PATCH 029/185] Improves caching on the allRelays call on the Nostr Client --- .../amethyst/ui/actions/RelaySelectionDialog.kt | 4 +--- .../quartz/nip01Core/relay/client/NostrClient.kt | 5 +---- .../quartz/nip01Core/relay/client/pool/RelayPool.kt | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt index 89639c7116..f95a045eee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt @@ -47,11 +47,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import com.google.common.collect.Multimaps.index import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton @@ -89,7 +87,7 @@ fun RelaySelectionDialog( ) { var relays by remember { mutableStateOf( - accountViewModel.account.client.connectedRelayList().map { + accountViewModel.account.client.allAvailableRelays().map { RelayList( relay = it, isSelected = preSelectedList.any { relayUrl -> it == relayUrl }, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index 0c61ccc7a1..56dcf79554 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -39,7 +39,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.forEach import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn @@ -89,7 +88,7 @@ class NostrClient( eventOutbox.forEachUnsentEvent(relay, liveRelay::send) } - fun connectedRelayList() = relayPool.getAll() + fun allAvailableRelays() = relayPool.getAll() // Reconnects all relays that may have disconnected fun connect() = relayPool.connect() @@ -239,7 +238,5 @@ class NostrClient( fun getSubscriptionFiltersOrNull(subId: String): List? = activeSubscriptions.getSubscriptionFiltersOrNull(subId) - fun connectedRelays() = relayPool.connectedRelays() - fun relayStatusFlow() = relayPool.statusFlow } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt index 484fe760b8..76b4d6f8b0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -54,7 +54,7 @@ class RelayPool( fun getRelay(url: NormalizedRelayUrl): IRelayClient? = relays.get(url) - fun getAll() = relays.keys() + fun getAll() = statusFlow.value.connected fun getAllNeedsToReconnect() = relays.filter { url, relay -> relay.needsToReconnect() } From cfbf113910a248d08aaf531da31e14117ea32797 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 13:08:27 -0400 Subject: [PATCH 030/185] Fixes crash on lazy flows when uploading images before the relay list is loaded. --- .../vitorpamplona/amethyst/model/Account.kt | 4 +--- .../FileStorageServerListState.kt | 2 +- .../nipB7Blossom/BlossomServerListState.kt | 2 +- .../model/serverList/MergedServerListState.kt | 20 +++++++++---------- .../mediaServers/BlossomServersViewModel.kt | 2 +- .../mediaServers/NIP96ServersViewModel.kt | 2 +- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 8a20110a82..1a2b26af04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -102,8 +102,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isOnion import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal @@ -260,7 +258,7 @@ class Account( val blossomServers = BlossomServerListState(signer, cache, scope, settings) val fileStorageServers = FileStorageServerListState(signer, cache, scope, settings) - val serverLists = MergedServerListState(fileStorageServers.fileServers, blossomServers.fileServers, scope) + val serverLists = MergedServerListState(fileStorageServers, blossomServers, scope) // Relay settings val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, scope) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt index 079e096bf5..bccbc05725 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt @@ -55,7 +55,7 @@ class FileStorageServerListState( return event?.servers() ?: emptyList() } - val fileServers = + val flow = getFileServersListFlow() .map { normalizeServers(it.note) } .onStart { emit(normalizeServers(getFileServersNote())) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt index 8cf1196f16..a85941bade 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt @@ -56,7 +56,7 @@ class BlossomServerListState( return event?.servers() ?: emptyList() } - val fileServers = + val flow = getBlossomServersListFlow() .map { normalizeServers(it.note) } .onStart { emit(normalizeServers(getBlossomServersNote())) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt index 599487b36f..578e912ed7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedServerListState.kt @@ -20,7 +20,8 @@ */ package com.vitorpamplona.amethyst.model.serverList -import android.R.attr.host +import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState +import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType @@ -30,14 +31,13 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import org.czeal.rfc3986.URIReference class MergedServerListState( - val fileServers: StateFlow>, - val blossomServers: StateFlow>, + val fileServers: FileStorageServerListState, + val blossomServers: BlossomServerListState, val scope: CoroutineScope, ) { fun host(url: String): String = @@ -59,15 +59,15 @@ class MergedServerListState( return result + ServerName("NIP95", "", ServerType.NIP95) } - val liveServerList: StateFlow> by lazy { - combine(fileServers, blossomServers) { nip96s, blossoms -> + val liveServerList: StateFlow> = + combine(fileServers.flow, blossomServers.flow) { nip96s, blossoms -> mergeServerList(nip96s, blossoms) - }.onStart { emit(mergeServerList(fileServers.value, blossomServers.value)) } - .flowOn(Dispatchers.Default) + }.onStart { + emit(mergeServerList(fileServers.flow.value, blossomServers.flow.value)) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, - emptyList(), + DEFAULT_MEDIA_SERVERS, ) - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt index 8172f7fea8..6eab5aadb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt @@ -127,5 +127,5 @@ class BlossomServersViewModel : ViewModel() { } } - private fun obtainFileServers(): List? = account.blossomServers.fileServers.value + private fun obtainFileServers(): List? = account.blossomServers.flow.value } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/NIP96ServersViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/NIP96ServersViewModel.kt index bd50c97e97..0d8c32b19a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/NIP96ServersViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/NIP96ServersViewModel.kt @@ -122,5 +122,5 @@ class NIP96ServersViewModel : ViewModel() { } } - private fun obtainFileServers(): List? = account.fileStorageServers.fileServers.value + private fun obtainFileServers(): List? = account.fileStorageServers.flow.value } From 64bad2b47616db76a852c7b97b9d582096f46841 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 13:08:50 -0400 Subject: [PATCH 031/185] Better logs for the basic relay --- .../client/single/basic/BasicRelayClient.kt | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index a476fc5761..7e48fac953 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -42,6 +42,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder @@ -63,6 +64,8 @@ open class BasicRelayClient( const val DELAY_TO_RECONNECT_IN_MSECS = 500L } + private val logTag = "Relay ${url.displayUrl()}" + private var socket: WebSocket? = null private var isReady: Boolean = false private var usingCompression: Boolean = false @@ -109,7 +112,7 @@ open class BasicRelayClient( return } - Log.d("Relay", "${url.url} connecting...") + Log.d(logTag, "Connecting...") lastConnectTentative = TimeUtils.now() @@ -118,7 +121,7 @@ open class BasicRelayClient( } catch (e: Exception) { if (e is CancellationException) throw e - Log.w("Relay", "${url.url} Crash before connecting $url", e) + Log.w(logTag, "Crash before connecting", e) stats.newError(e.message ?: "Error trying to connect: ${e.javaClass.simpleName}") markConnectionAsClosed() @@ -135,7 +138,7 @@ open class BasicRelayClient( pingMillis: Long, compression: Boolean, ) { - Log.d("Relay", "${url.url} onConnect $socket") + Log.d(logTag, "OnOpen (ping: ${pingMillis}ms${if (compression) ", using compression" else ""})") markConnectionAsReady(pingMillis, compression) @@ -145,6 +148,8 @@ open class BasicRelayClient( } override fun onMessage(text: String) { + // Log.d(logTag, "Receiving: $text") + stats.addBytesReceived(text.bytesUsedInMemory()) try { @@ -161,7 +166,7 @@ open class BasicRelayClient( } catch (e: Throwable) { if (e is CancellationException) throw e stats.newError("Error processing: $text") - Log.e("Relay", "${url.url} Error processing: $text") + Log.e(logTag, "Error processing: $text") listener.onError(this@BasicRelayClient, "", Error("Error processing $text")) } } @@ -170,7 +175,7 @@ open class BasicRelayClient( code: Int, reason: String, ) { - Log.w("Relay", "${url.url} onClosing: $code $reason") + Log.w(logTag, "OnClosing $code $reason") listener.onRelayStateChange(this@BasicRelayClient, RelayState.DISCONNECTING) } @@ -179,10 +184,9 @@ open class BasicRelayClient( code: Int, reason: String, ) { + Log.w(logTag, "OnClosed $reason") + markConnectionAsClosed() - - Log.w("Relay", "${url.url} onClosed $reason") - listener.onRelayStateChange(this@BasicRelayClient, RelayState.DISCONNECTED) } @@ -201,7 +205,7 @@ open class BasicRelayClient( // Failures disconnect the relay. markConnectionAsClosed() - Log.w("Relay", "${url.url} onFailure $code $response ${t.message} $socket") + Log.w(logTag, "OnFailure $code $response ${t.message} $socket") t.printStackTrace() listener.onError( this@BasicRelayClient, @@ -233,6 +237,7 @@ open class BasicRelayClient( } private fun processEvent(msg: EventMessage) { + // Log.w(logTag, "Event ${msg.subId} ${msg.event.toJson()}") listener.onEvent( relay = this, subId = msg.subId, @@ -243,13 +248,13 @@ open class BasicRelayClient( } private fun processEose(msg: EoseMessage) { - // Log.w("Relay", "Relay onEOSE $url $newMessage") + // Log.w(logTag, "EOSE ${msg.subId}") afterEOSEPerSubscription[msg.subId] = true listener.onEOSE(this, msg.subId, TimeUtils.now()) } private fun processNotice(msg: NoticeMessage) { - // Log.w("Relay", "Relay onNotice $url, $newMessage") + Log.w(logTag, "Notice ${msg.message}") stats.newNotice(msg.message) listener.onError(this@BasicRelayClient, msg.message, Error("Relay sent notice: $msg.message")) } @@ -258,7 +263,7 @@ open class BasicRelayClient( msg: OkMessage, onConnected: () -> Unit, ) { - Log.w("Relay", "${url.url} onOK: ${msg.eventId} ${msg.success} ${msg.message}") + // Log.w(logTag, "OK: ${msg.eventId} ${msg.success} ${msg.message}") // if this is the OK of an auth event, renew all subscriptions and resend all outgoing events. if (authResponseWatcher.containsKey(msg.eventId)) { @@ -278,29 +283,29 @@ open class BasicRelayClient( } private fun processAuth(msg: AuthMessage) { - // Log.d("Relay", "Relay onAuth $url, $newMessage") + // Log.d(logTag, "Auth $newMessage") listener.onAuth(this@BasicRelayClient, msg.challenge) } private fun processNotify(msg: NotifyMessage) { - // Log.w("Relay", "Relay onNotify $url, $newMessage") + // Log.w(logTag, "Notify $newMessage") listener.onNotify(this@BasicRelayClient, msg.message) } private fun processClosed(msg: ClosedMessage) { + // Log.w(logTag, "Relay Closed Subscription $newMessage") afterEOSEPerSubscription[msg.subscriptionId] = false - // Log.w("Relay", "Relay Closed Subscription $url, $newMessage") listener.onClosed(this@BasicRelayClient, msg.subscriptionId, msg.message) } private fun processUnkownMessage(newMessage: String) { stats.newError("Unsupported message: $newMessage") - Log.w("Relay", "Unsupported message: $newMessage") + Log.w(logTag, "Unsupported message: $newMessage") listener.onError(this, "", Error("Unsupported message: $newMessage")) } override fun disconnect() { - Log.d("Relay", "${url.url} disconnecting...") + Log.d(logTag, "Disconnecting...") lastConnectTentative = 0L // this is not an error, so prepare to reconnect as soon as requested. delayToConnect = DELAY_TO_RECONNECT_IN_MSECS socket?.disconnect() @@ -410,6 +415,7 @@ open class BasicRelayClient( ) } socket?.let { + // Log.d(logTag, "Sending: $str") val result = it.send(str) listener.onSend(this@BasicRelayClient, str, result) stats.addBytesSent(str.bytesUsedInMemory()) @@ -417,7 +423,10 @@ open class BasicRelayClient( } override fun close(subscriptionId: String) { - writeToSocket(CloseCmd.Companion.toJson(subscriptionId)) - afterEOSEPerSubscription[subscriptionId] = false + // avoids sending closes for subscriptions that were never sent to this relay. + if (afterEOSEPerSubscription.containsKey(subscriptionId)) { + writeToSocket(CloseCmd.Companion.toJson(subscriptionId)) + afterEOSEPerSubscription[subscriptionId] = false + } } } From e4ebca29f4c5844be24f587381dce233f397f956 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 15:45:46 -0400 Subject: [PATCH 032/185] Improves RelayHint normalization to make sure the NormalizedRelayUrls are indeed relays --- .../relay/normalizer/RelayUrlNormalizer.kt | 68 ++++++-- .../quartz/nip01Core/tags/events/ETag.kt | 33 +++- .../quartz/nip01Core/tags/people/PTag.kt | 14 +- .../quartz/nip10Notes/tags/MarkedETag.kt | 158 ++++++++++-------- .../quartz/nip18Reposts/quotes/QTag.kt | 22 ++- 5 files changed, 199 insertions(+), 96 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index 401f6bf0ee..6af430c1da 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -20,8 +20,12 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.normalizer +import android.util.Log +import android.util.Log.e import androidx.collection.LruCache +import kotlinx.coroutines.CancellationException import org.czeal.rfc3986.URIReference +import kotlin.contracts.ExperimentalContracts val normalizedUrls = LruCache(5000) @@ -37,6 +41,27 @@ class RelayUrlNormalizer { fun isOnion(url: String) = url.endsWith(".onion") || url.contains(".onion/") + fun isRelaySchemePrefix(url: String) = url.length > 6 && url[0] == 'w' && url[1] == 's' + + fun isRelaySchemePrefixSecure(url: String) = url[2] == 's' && url[3] == ':' && url[4] == '/' && url[5] == '/' && url[6] != '/' + + fun isRelaySchemePrefixInsecure(url: String) = url[2] == ':' && url[3] == '/' && url[4] == '/' && url[5] != '/' + + fun isRelayUrl(url: String): Boolean { + val trimmed = url.trim().ifEmpty { return false } + + // fast + if (isRelaySchemePrefix(trimmed)) { + if (isRelaySchemePrefixSecure(trimmed)) { + return true + } else if (isRelaySchemePrefixInsecure(trimmed)) { + return true + } + } + + return false + } + private fun norm(url: String) = NormalizedRelayUrl( URIReference @@ -46,19 +71,20 @@ class RelayUrlNormalizer { .intern(), ) - fun fix(url: String): String { - val trimmed = url.trim() + @OptIn(ExperimentalContracts::class) + fun fix(url: String): String? { + val trimmed = url.trim().ifEmpty { return null } - // fast - if (trimmed.length > 4 && trimmed[0] == 'w' && trimmed[1] == 's') { - if (trimmed[2] == 's' && trimmed[3] == ':' && trimmed[4] == '/' && trimmed[5] == '/') { - return trimmed - } else if (trimmed[2] == ':' && trimmed[3] == '/' && trimmed[4] == '/') { + if (trimmed.isEmpty()) return null + + // fast for good wss:// urls + if (isRelaySchemePrefix(trimmed)) { + if (isRelaySchemePrefixSecure(trimmed) || isRelaySchemePrefixInsecure(trimmed)) { return trimmed } } - // fast + // fast for good https:// urls if (trimmed.length > 8 && trimmed[0] == 'h' && trimmed[1] == 't' && trimmed[2] == 't' && trimmed[3] == 'p') { if (trimmed[4] == 's' && trimmed[5] == ':' && trimmed[6] == '/' && trimmed[7] == '/') { // https:// @@ -69,6 +95,12 @@ class RelayUrlNormalizer { } } + if (trimmed.contains("://")) { + // some other scheme we cannot connect to. + Log.w("RelayUrlNormalizer", "Rejected relay URL: $url") + return null + } + return if (isOnion(trimmed) || isLocalHost(trimmed)) { "ws://$trimmed" } else { @@ -80,7 +112,8 @@ class RelayUrlNormalizer { normalizedUrls.get(url)?.let { return it } return try { - val normalized = norm(fix(url)) + val fixed = fix(url) ?: return NormalizedRelayUrl(url) + val normalized = norm(fixed) normalizedUrls.put(url, normalized) normalized } catch (e: Exception) { @@ -89,13 +122,22 @@ class RelayUrlNormalizer { } fun normalizeOrNull(url: String): NormalizedRelayUrl? { - normalizedUrls.get(url)?.let { return it } + if (url.isEmpty()) return null + normalizedUrls[url]?.let { return it } return try { - val normalized = norm(fix(url)) - normalizedUrls.put(url, normalized) - normalized + val fixed = fix(url) + if (fixed != null) { + val normalized = norm(fixed) + normalizedUrls.put(url, normalized) + return normalized + } else { + Log.w("NormalizedRelayUrl", "Rejected Error $url") + null + } } catch (e: Exception) { + if (e is CancellationException) throw e + Log.w("NormalizedRelayUrl", "Rejected Error $url") null } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt index f9901163f1..181aa88ac7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.events +import android.R.attr.tag import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has @@ -72,8 +73,7 @@ data class ETag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } - return ETag(tag[1], hint, tag.getOrNull(3)) + return ETag(tag[1], pickRelayHint(tag), pickAuthor(tag)) } @JvmStatic @@ -84,6 +84,32 @@ data class ETag( return tag[1] } + // simple case ["e", "id", "relay"] + // empty tags ["e", "id", "relay", ""] + // current root ["e", "id", "relay", "marker"] + // current root ["e", "id", "relay", "marker", "pubkey"] + // empty tags ["e", "id", "relay", "", "pubkey"] + // pubkey marker ["e", "id", "relay", "pubkey"] + // pubkey marker ["e", "id", "relay", "pubkey", "marker"] + // pubkey marker ["e", "id", "pubkey"] // incorrect + // current root ["e", "id", "marker"] // incorrect + + @JvmStatic + private fun pickRelayHint(tag: Array): NormalizedRelayUrl? { + if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2]) + if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3]) + if (tag.has(4) && tag[4].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[4])) return RelayUrlNormalizer.normalizeOrNull(tag[4]) + return null + } + + @JvmStatic + private fun pickAuthor(tag: Array): HexKey? { + if (tag.has(2) && tag[2].length == 64) return tag[2] + if (tag.has(3) && tag[3].length == 64) return tag[3] + if (tag.has(4) && tag[4].length == 64) return tag[4] + return null + } + @JvmStatic fun parseAsHint(tag: Array): EventIdHint? { ensure(tag.has(2)) { return null } @@ -91,7 +117,8 @@ data class ETag( ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + val hint = pickRelayHint(tag) + ensure(hint != null) { return null } return EventIdHint(tag[1], hint) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt index 4695d5b1ff..2f8cda9fd1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt @@ -66,11 +66,16 @@ data class PTag( ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + val hint = pickRelayHint(tag) return PTag(tag[1], hint) } + private fun pickRelayHint(tag: Array): NormalizedRelayUrl? { + if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2]) + return null + } + @JvmStatic fun parseKey(tag: Array): HexKey? { ensure(tag.has(1)) { return null } @@ -89,10 +94,11 @@ data class PTag( ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - val normalized = RelayUrlNormalizer.normalizeOrNull(tag[2]) - ensure(normalized != null) { return null } + val hint = pickRelayHint(tag) - return PubKeyHint(tag[1], normalized) + ensure(hint != null) { return null } + + return PubKeyHint(tag[1], hint) } @JvmStatic diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt index ece41e8b21..576b04b178 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt @@ -20,12 +20,14 @@ */ package com.vitorpamplona.quartz.nip10Notes.tags +import android.R.attr.tag import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer.Companion.isRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull @@ -87,67 +89,71 @@ data class MarkedETag( @JvmStatic fun parse(tag: Array): MarkedETag? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } return MarkedETag( - tag[ORDER_EVT_ID], - RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), - MARKER.parse(tag[ORDER_MARKER]), - tag.getOrNull(ORDER_PUBKEY), + eventId = tag[1], + relayHint = pickRelayHint(tag), + marker = pickMarker(tag), + authorPubKeyHex = pickAuthor(tag), ) } @JvmStatic fun parseId(tag: Array): HexKey? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } - return tag[ORDER_EVT_ID] + return tag[1] + } + + // simple case ["e", "id", "relay"] + // empty tags ["e", "id", "relay", ""] + // current root ["e", "id", "relay", "marker"] + // current root ["e", "id", "relay", "marker", "pubkey"] + // empty tags ["e", "id", "relay", "", "pubkey"] + // pubkey marker ["e", "id", "relay", "pubkey"] + // pubkey marker ["e", "id", "relay", "pubkey", "marker"] + // pubkey marker ["e", "id", "pubkey"] // incorrect + // current root ["e", "id", "marker"] // incorrect + + @JvmStatic + private fun pickRelayHint(tag: Array): NormalizedRelayUrl? { + if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2]) + if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3]) + if (tag.has(4) && tag[4].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[4])) return RelayUrlNormalizer.normalizeOrNull(tag[4]) + return null + } + + @JvmStatic + private fun pickAuthor(tag: Array): HexKey? { + if (tag.has(3) && tag[3].length == 64) return tag[3] + if (tag.has(4) && tag[4].length == 64) return tag[4] + if (tag.has(2) && tag[2].length == 64) return tag[2] + return null + } + + @JvmStatic + private fun pickMarker(tag: Array): MARKER? { + if (tag.has(3)) MARKER.parse(tag[3])?.let { return it } + if (tag.has(4)) MARKER.parse(tag[4])?.let { return it } + if (tag.has(2)) MARKER.parse(tag[2])?.let { return it } + return null } @JvmStatic fun parseAllThreadTags(tag: Array): MarkedETag? = if (tag.size >= 2 && tag[0] == TAG_NAME) { - if (tag.size <= 3) { - // simple case ["e", "id", "relay"] - MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, null, null) - } else if (tag.size == 4) { - val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) - if (tag[3].isEmpty()) { - // empty tags ["e", "id", "relay", ""] - MarkedETag(tag[1], relayHint, null, null) - } else if (tag[3].length == 64) { - // updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"] - MarkedETag(tag[1], relayHint, null, tag[3]) - } else if (tag[3] == MARKER.ROOT.code) { - // corrent root ["e", "id", "relay", "root"] - MarkedETag(tag[1], relayHint, MARKER.ROOT) - } else if (tag[3] == MARKER.REPLY.code) { - // correct reply ["e", "id", "relay", "reply"] - MarkedETag(tag[1], relayHint, MARKER.REPLY) - } else { - // ignore "mention" and "fork" markers - null - } - } else { - val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) - // tag.size >= 5 - if (tag[3].isEmpty()) { - // empty tags ["e", "id", "relay", "", "pubkey"] - MarkedETag(tag[1], relayHint, null, tag[4]) - } else if (tag[3].length == 64) { - // updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"] - MarkedETag(tag[1], relayHint, null, tag[3]) - } else if (tag[3] == MARKER.ROOT.code) { - // corrent root ["e", "id", "relay", "root"] - MarkedETag(tag[1], relayHint, MARKER.ROOT, tag[4]) - } else if (tag[3] == MARKER.REPLY.code) { - // correct reply ["e", "id", "relay", "reply"] - MarkedETag(tag[1], relayHint, MARKER.REPLY, tag[4]) - } else { - // ignore "mention" and "fork" markers - null - } - } + MarkedETag( + eventId = tag[1], + relayHint = pickRelayHint(tag), + marker = pickMarker(tag), + authorPubKeyHex = pickAuthor(tag), + ) } else { null } @@ -187,9 +193,8 @@ data class MarkedETag( ensure(tag.has(2)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } - ensure(tag[2].isNotEmpty()) { return null } - val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + val hint = pickRelayHint(tag) ensure(hint != null) { return null } return EventIdHint(tag[1], hint) @@ -197,15 +202,18 @@ data class MarkedETag( @JvmStatic fun parseRoot(tag: Array): MarkedETag? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null - if (tag[ORDER_MARKER] != MARKER.ROOT.code) return null + ensure(tag.has(3)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + val marker = pickMarker(tag) + ensure(marker == MARKER.ROOT) { return null } - // ["e", id hex, relay hint, marker, pubkey] return MarkedETag( - eventId = tag[ORDER_EVT_ID], - relayHint = RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), - marker = MARKER.ROOT, - authorPubKeyHex = tag.getOrNull(ORDER_PUBKEY), + eventId = tag[1], + relayHint = pickRelayHint(tag), + marker = marker, + authorPubKeyHex = pickAuthor(tag), ) } @@ -215,23 +223,25 @@ data class MarkedETag( @JvmStatic fun parseUnmarkedRoot(tag: Array): MarkedETag? = if (tag.size in 2..3 && tag[0] == TAG_NAME) { - MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, MARKER.ROOT) + MarkedETag(tag[1], pickRelayHint(tag), MARKER.ROOT) } else { null } @JvmStatic fun parseReply(tag: Array): MarkedETag? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null - if (tag[ORDER_MARKER] != MARKER.REPLY.code) return null - // ["e", id hex, relay hint, marker, pubkey] + ensure(tag.has(3)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + val marker = pickMarker(tag) + ensure(marker == MARKER.REPLY) { return null } + return MarkedETag( - tag[ORDER_EVT_ID], - RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), - MARKER.REPLY, - tag.getOrNull( - ORDER_PUBKEY, - ), + eventId = tag[1], + relayHint = pickRelayHint(tag), + marker = marker, + authorPubKeyHex = pickAuthor(tag), ) } @@ -241,17 +251,21 @@ data class MarkedETag( @JvmStatic fun parseUnmarkedReply(tag: Array): MarkedETag? = if (tag.size in 2..3 && tag[0] == TAG_NAME) { - MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, MARKER.REPLY) + MarkedETag(tag[1], pickRelayHint(tag), MARKER.REPLY) } else { null } @JvmStatic fun parseRootId(tag: Array): HexKey? { - if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null - if (tag[ORDER_MARKER] != MARKER.ROOT.code) return null - // ["e", id hex, relay hint, marker, pubkey] - return tag[ORDER_EVT_ID] + ensure(tag.has(3)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + val marker = pickMarker(tag) + ensure(marker == MARKER.ROOT) { return null } + + return tag[1] } @JvmStatic diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt index f059e9a90e..ae275a8dc8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt @@ -20,9 +20,11 @@ */ package com.vitorpamplona.quartz.nip18Reposts.quotes +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.utils.ensure @@ -38,16 +40,28 @@ interface QTag { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } - val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + val relayHint = pickRelayHint(tag) return if (tag[1].length == 64) { - QEventTag(tag[1], relayHint, tag.getOrNull(3)) + QEventTag(tag[1], relayHint, pickAuthor(tag)) } else { val address = Address.parse(tag[1]) ?: return null QAddressableTag(address, relayHint) } } + private fun pickRelayHint(tag: Array): NormalizedRelayUrl? { + if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2]) + if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3]) + return null + } + + private fun pickAuthor(tag: Array): HexKey? { + if (tag.has(2) && tag[2].length == 64) return tag[2] + if (tag.has(3) && tag[3].length == 64) return tag[3] + return null + } + @JvmStatic fun parseKey(tag: Array): String? { ensure(tag.has(1)) { return null } @@ -62,7 +76,7 @@ interface QTag { ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } - val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + val relayHint = pickRelayHint(tag) ensure(relayHint != null) { return null } return EventIdHint(tag[1], relayHint) @@ -76,7 +90,7 @@ interface QTag { ensure(tag[2].isNotEmpty()) { return null } ensure(!tag[1].contains(':')) { return null } - val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + val relayHint = pickRelayHint(tag) ensure(relayHint != null) { return null } return AddressHint(tag[1], relayHint) From 9e72f5d55edb3ffe6aeebf6a5b2f4fba0b0ee388 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 17:14:07 -0400 Subject: [PATCH 033/185] Fixes icon size cache on the User Profile --- .../amethyst/ui/StringResourceCache.kt | 22 +++- .../amethyst/ui/actions/EditPostView.kt | 2 +- .../amethyst/ui/actions/NewMediaView.kt | 2 +- .../ui/components/ZoomableContentView.kt | 22 +++- .../components/toasts/multiline/ErrorList.kt | 2 +- .../amethyst/ui/layouts/ChatHeaderLayout.kt | 4 +- .../amethyst/ui/layouts/LeftPictureLayout.kt | 2 +- .../amethyst/ui/navigation/AppBottomBar.kt | 2 +- .../amethyst/ui/navigation/DrawerContent.kt | 14 ++- .../amethyst/ui/note/ErrorMessageDialog.kt | 2 +- .../vitorpamplona/amethyst/ui/note/Icons.kt | 110 ------------------ .../amethyst/ui/note/MessageSetCompose.kt | 11 +- .../ui/note/NIP05VerificationDisplay.kt | 38 ++++-- .../amethyst/ui/note/NoteQuickActionMenu.kt | 6 +- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 9 +- .../ui/note/creators/zappolls/PollField.kt | 2 +- .../ui/note/elements/DefaultImageHeader.kt | 4 +- .../nip22Comments/GenericCommentPostScreen.kt | 2 +- .../amethyst/ui/note/types/AppDefinition.kt | 2 +- .../chats/privateDM/IncognitoBadge.kt | 10 +- .../chats/privateDM/send/ToggleNip17Button.kt | 26 ++--- .../communities/NewCommunityNoteButton.kt | 8 +- .../nip99Classifieds/NewProductScreen.kt | 2 +- .../loggedIn/geohash/NewGeoNoteButton.kt | 8 +- .../loggedIn/hashtag/NewHashtagNoteButton.kt | 8 +- .../ui/screen/loggedIn/home/NewNoteButton.kt | 8 +- .../loggedIn/home/ShortNotePostScreen.kt | 19 ++- .../loggedIn/keyBackup/AccountBackupDialog.kt | 2 +- .../profile/header/DrawAdditionalInfo.kt | 13 ++- .../loggedIn/profile/header/DrawBanner.kt | 4 +- .../loggedIn/profile/header/MessageButton.kt | 2 +- .../relays/common/RelayNameAndRemoveButton.kt | 4 +- .../loggedIn/threadview/ThreadFeedView.kt | 2 +- .../screen/loggedIn/video/NewImageButton.kt | 7 +- .../ui/screen/loggedOff/LoginScreen.kt | 2 +- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 6 +- 36 files changed, 173 insertions(+), 216 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/StringResourceCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/StringResourceCache.kt index c1a25736f6..55430dd5f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/StringResourceCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/StringResourceCache.kt @@ -37,7 +37,7 @@ private val resourceCache = LruCache(300) private var resourceCacheLanguage: String? = null // Caches most common icons in the app to avoid using disk -private val iconCache = LruCache(30) +private val iconCache = LruCache>(30) fun checkLanguage(currentLanguage: String) { if (resourceCacheLanguage == null) { @@ -127,19 +127,29 @@ fun stringRes( /** * This cache can only be used if the painter is the only copy on the screen + * It should store a separate Painter for each size. It's safe to just assume + * Different compositions use different sizes. */ @Composable fun painterRes( - @DrawableRes id: Int, + @DrawableRes resourceId: Int, + sizeReference: Int, ): Painter { - val cached = iconCache.get(id) + val cached = iconCache.get(resourceId) if (cached != null) { - return cached + val composition = cached.get(sizeReference) + if (composition != null) { + return composition + } } - val loaded = painterResource(id) + val loaded = painterResource(resourceId) - iconCache.put(id, loaded) + if (cached == null) { + iconCache.put(resourceId, LruCache(10)) + } else { + cached.put(sizeReference, loaded) + } return loaded } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 199e2d236a..557c8a8785 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -175,7 +175,7 @@ fun EditPostView( onClick = { showRelaysDialog = true }, ) { Icon( - painter = painterRes(R.drawable.relays), + painter = painterRes(R.drawable.relays, 2), contentDescription = stringRes(id = R.string.relay_list_selector), modifier = Modifier.height(25.dp), tint = MaterialTheme.colorScheme.onBackground, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index fb81e50822..9252a51dc9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -134,7 +134,7 @@ fun NewMediaView( onClick = { showRelaysDialog = true }, ) { Icon( - painter = painterRes(R.drawable.relays), + painter = painterRes(R.drawable.relays, 3), contentDescription = stringRes(id = R.string.relay_list_selector), modifier = Modifier.height(25.dp), tint = MaterialTheme.colorScheme.onBackground, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index ea77ff3b9b..1b79f32b33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -36,8 +36,11 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Report import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -86,13 +89,12 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon -import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon -import com.vitorpamplona.amethyst.ui.note.HashCheckIcon +import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.Size24dp -import com.vitorpamplona.amethyst.ui.theme.Size30dp +import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.Size6dp import com.vitorpamplona.amethyst.ui.theme.Size75dp @@ -837,7 +839,12 @@ private fun HashVerificationSymbol(verifiedHash: Boolean) { openDialogMsg.value = stringRes(localContext, R.string.hash_verification_passed) }, ) { - HashCheckIcon(Size30dp) + Icon( + painter = painterRes(R.drawable.original, 1), + contentDescription = stringRes(id = R.string.hash_verification_passed), + modifier = Size30Modifier, + tint = Color.Unspecified, + ) } } else { IconButton( @@ -846,7 +853,12 @@ private fun HashVerificationSymbol(verifiedHash: Boolean) { openDialogMsg.value = stringRes(localContext, R.string.hash_verification_failed) }, ) { - HashCheckFailedIcon(Size30dp) + Icon( + imageVector = Icons.Default.Report, + contentDescription = stringRes(id = R.string.hash_verification_failed), + modifier = Size30Modifier, + tint = Color.Red, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt index 89dabc657d..826f216876 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt @@ -140,7 +140,7 @@ fun ErrorRow( } ?: stringRes(R.string.error_dialog_talk_to_user) Icon( - painter = painterRes(R.drawable.ic_dm), + painter = painterRes(R.drawable.ic_dm, 2), contentDescription = descriptor, modifier = Size20Modifier, tint = MaterialTheme.colorScheme.primary, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt index 9cc2040462..204e28356d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/ChatHeaderLayout.kt @@ -56,7 +56,7 @@ fun ChannelNamePreview() { ChatHeaderLayout( channelPicture = { Image( - painter = painterRes(R.drawable.github), + painter = painterRes(R.drawable.github, 1), contentDescription = stringRes(id = R.string.profile_banner), contentScale = ContentScale.FillWidth, ) @@ -89,7 +89,7 @@ fun ChannelNamePreview() { }, leadingContent = { Image( - painter = painterRes(R.drawable.github), + painter = painterRes(R.drawable.github, 2), contentDescription = stringRes(id = R.string.profile_banner), contentScale = ContentScale.FillWidth, modifier = Size55Modifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt index 51dac9e05d..50a4a6c6f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/layouts/LeftPictureLayout.kt @@ -65,7 +65,7 @@ fun LeftPictureLayoutPreviewCard() { LeftPictureLayout( onImage = { Image( - painter = painterRes(R.drawable.github), + painter = painterRes(R.drawable.github, 3), contentDescription = stringRes(id = R.string.profile_banner), contentScale = ContentScale.FillWidth, modifier = Modifier.fillMaxSize().clip(QuoteBorder), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index dd57aefdcf..2c6eed373d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -184,7 +184,7 @@ private fun NotifiableIcon( ) { Box(route.notifSize) { Icon( - painter = painterRes(id = route.icon), + painter = painterRes(resourceId = route.icon, 0), contentDescription = stringRes(route.contentDescriptor), modifier = route.iconSize, tint = if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 4a87c19704..7799af097b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -225,7 +225,7 @@ fun ProfileContentTemplate( ) } else { Image( - painter = painterRes(R.drawable.profile_banner), + painter = painterRes(R.drawable.profile_banner, 3), contentDescription = stringRes(R.string.profile_banner), contentScale = ContentScale.FillWidth, modifier = bannerModifier, @@ -480,6 +480,7 @@ fun ListContent( NavigationRow( title = R.string.privacy_options, icon = R.drawable.ic_tor, + iconReference = 1, tint = MaterialTheme.colorScheme.onBackground, nav = nav, route = Route.PrivacyOptions, @@ -567,6 +568,7 @@ private fun RenderRelayStatus(relayPool: RelayPool.RelayPoolStatus) { fun NavigationRow( title: Int, icon: Int, + iconReference: Int, tint: Color, nav: INav, route: Route, @@ -574,6 +576,7 @@ fun NavigationRow( IconRow( title, icon, + iconReference, tint, onClick = { nav.closeDrawer() @@ -622,6 +625,7 @@ fun NavigationRow( fun IconRow( title: Int, icon: Int, + iconReference: Int, tint: Color, onClick: () -> Unit, ) { @@ -638,7 +642,7 @@ fun IconRow( verticalAlignment = Alignment.CenterVertically, ) { Icon( - painter = painterRes(icon), + painter = painterRes(icon, iconReference), contentDescription = stringRes(title), modifier = Size22Modifier, tint = tint, @@ -724,8 +728,8 @@ fun IconRowRelays( verticalAlignment = Alignment.CenterVertically, ) { Icon( - painter = painterRes(R.drawable.relays), - null, + painter = painterRes(R.drawable.relays, 4), + contentDescription = stringRes(R.string.relay_setup), modifier = Modifier.size(22.dp), tint = MaterialTheme.colorScheme.onSurface, ) @@ -800,7 +804,7 @@ fun BottomContent( }, ) { Icon( - painter = painterRes(R.drawable.ic_qrcode), + painter = painterRes(R.drawable.ic_qrcode, 2), contentDescription = stringRes(id = R.string.show_npub_as_a_qr_code), modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt index 5f0f3292d7..cc9fb379f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ErrorMessageDialog.kt @@ -81,7 +81,7 @@ fun ErrorMessageDialog( onClickStartMessage?.let { TextButton(onClick = onClickStartMessage) { Icon( - painter = painterRes(R.drawable.ic_dm), + painter = painterRes(R.drawable.ic_dm, 3), contentDescription = null, ) Spacer(StdHorzSpacer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index abd01796a8..d331ef3544 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -31,13 +31,11 @@ import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.DownloadForOffline -import androidx.compose.material.icons.filled.Downloading import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.PushPin -import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.outlined.AddReaction import androidx.compose.material.icons.outlined.Close @@ -49,7 +47,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import com.vitorpamplona.amethyst.R @@ -64,7 +61,6 @@ import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.commons.icons.Reposted import com.vitorpamplona.amethyst.commons.icons.Search import com.vitorpamplona.amethyst.commons.icons.Zap -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.Size19Modifier @@ -108,16 +104,6 @@ fun ArrowBackIcon(tint: Color = MaterialTheme.colorScheme.grayText) { ) } -@Composable -fun MessageIcon(modifier: Modifier) { - Icon( - painter = painterRes(R.drawable.ic_dm), - null, - modifier = modifier, - tint = MaterialTheme.colorScheme.primary, - ) -} - @Composable fun DownloadForOfflineIcon( iconSize: Dp, @@ -131,26 +117,6 @@ fun DownloadForOfflineIcon( ) } -@Composable -fun HashCheckIcon(iconSize: Dp) { - Icon( - painter = painterRes(R.drawable.original), - contentDescription = stringRes(id = R.string.hash_verification_passed), - modifier = remember(iconSize) { Modifier.size(iconSize) }, - tint = Color.Unspecified, - ) -} - -@Composable -fun HashCheckFailedIcon(iconSize: Dp) { - Icon( - imageVector = Icons.Default.Report, - contentDescription = stringRes(id = R.string.hash_verification_failed), - modifier = remember(iconSize) { Modifier.size(iconSize) }, - tint = Color.Red, - ) -} - @Composable fun LikedIcon( modifier: Modifier, @@ -361,26 +327,6 @@ fun CommentIcon( ) } -@Composable -fun PollIcon() { - Icon( - painter = painterRes(R.drawable.ic_poll), - contentDescription = stringRes(id = R.string.poll), - modifier = Size20Modifier, - tint = MaterialTheme.colorScheme.onBackground, - ) -} - -@Composable -fun RegularPostIcon() { - Icon( - painter = painterRes(R.drawable.ic_lists), - contentDescription = stringRes(id = R.string.disable_poll), - modifier = Size20Modifier, - tint = MaterialTheme.colorScheme.onBackground, - ) -} - @Composable fun CancelIcon() { Icon( @@ -482,59 +428,3 @@ fun VerticalDotsIcon() { tint = MaterialTheme.colorScheme.placeholderText, ) } - -@Composable -fun NIP05CheckingIcon(modifier: Modifier) { - Icon( - imageVector = Icons.Default.Downloading, - contentDescription = stringRes(id = R.string.nip05_checking), - modifier = modifier, - tint = Color.Yellow, - ) -} - -@Composable -fun NIP05VerifiedIcon(modifier: Modifier) { - Icon( - painter = painterRes(R.drawable.nip_05), - contentDescription = stringRes(id = R.string.nip05_verified), - modifier = modifier, - tint = Color.Unspecified, - ) -} - -@Composable -fun NIP05FailedVerification(modifier: Modifier) { - Icon( - imageVector = Icons.Default.Report, - contentDescription = stringRes(id = R.string.nip05_failed), - modifier = modifier, - tint = Color.Red, - ) -} - -@Composable -fun IncognitoIconOn( - modifier: Modifier, - tint: Color, -) { - Icon( - painter = painterResource(id = R.drawable.incognito), - contentDescription = stringRes(id = R.string.accessibility_turn_off_sealed_message), - modifier = modifier, - tint = tint, - ) -} - -@Composable -fun IncognitoIconOff( - modifier: Modifier, - tint: Color, -) { - Icon( - painter = painterResource(id = R.drawable.incognito_off), - contentDescription = stringRes(id = R.string.accessibility_turn_on_sealed_message), - modifier = modifier, - tint = tint, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index be83c4641f..92e50947d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -30,6 +30,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -37,9 +39,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu +import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.MessageSetCard import com.vitorpamplona.amethyst.ui.theme.StdStartPadding @@ -116,6 +120,11 @@ fun MessageSetCompose( @Composable fun MessageIconBox() { Box(Modifier.width(55.dp).padding(top = 5.dp, end = 5.dp)) { - MessageIcon(Modifier.size(16.dp).align(Alignment.TopEnd)) + Icon( + painter = painterRes(R.drawable.ic_dm, 4), + null, + modifier = Modifier.size(16.dp).align(Alignment.TopEnd), + tint = MaterialTheme.colorScheme.primary, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index aca67bf2fd..b859a1d38c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -26,6 +26,8 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.Downloading +import androidx.compose.material.icons.filled.Report import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle @@ -41,11 +43,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.hashtags.Tunestr import com.vitorpamplona.amethyst.model.AddressableNote @@ -59,11 +63,10 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote -import com.vitorpamplona.amethyst.ui.note.NIP05CheckingIcon -import com.vitorpamplona.amethyst.ui.note.NIP05FailedVerification -import com.vitorpamplona.amethyst.ui.note.NIP05VerifiedIcon import com.vitorpamplona.amethyst.ui.note.WatchAuthor +import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.NIP05IconSize import com.vitorpamplona.amethyst.ui.theme.Size15Modifier @@ -389,7 +392,7 @@ fun DisplayNIP05( ) } - NIP05VerifiedSymbol(nip05Verified, NIP05IconSize, accountViewModel) + NIP05VerifiedSymbol(nip05Verified, 1, NIP05IconSize, accountViewModel) ClickableTextPrimary( text = domain, @@ -404,14 +407,33 @@ fun DisplayNIP05( @Composable private fun NIP05VerifiedSymbol( nip05Verified: MutableState, + compositionSizeReference: Int, modifier: Modifier, accountViewModel: AccountViewModel, ) { CrossfadeIfEnabled(targetState = nip05Verified.value, accountViewModel = accountViewModel) { when (it) { - null -> NIP05CheckingIcon(modifier = modifier) - true -> NIP05VerifiedIcon(modifier = modifier) - false -> NIP05FailedVerification(modifier = modifier) + null -> + Icon( + imageVector = Icons.Default.Downloading, + contentDescription = stringRes(id = R.string.nip05_checking), + modifier = modifier, + tint = Color.Yellow, + ) + true -> + Icon( + painter = painterRes(R.drawable.nip_05, compositionSizeReference), + contentDescription = stringRes(id = R.string.nip05_verified), + modifier = modifier, + tint = Color.Unspecified, + ) + false -> + Icon( + imageVector = Icons.Default.Report, + contentDescription = stringRes(id = R.string.nip05_failed), + modifier = modifier, + tint = Color.Red, + ) } } } @@ -427,7 +449,7 @@ fun DisplayNip05ProfileStatus( if (nip05.split("@").size <= 2) { val nip05Verified = nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel) Row(verticalAlignment = Alignment.CenterVertically) { - NIP05VerifiedSymbol(nip05Verified, Size16Modifier, accountViewModel) + NIP05VerifiedSymbol(nip05Verified, 2, Size16Modifier, accountViewModel) var domainPadStart = 5.dp val (user, domain) = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index 12db18fcba..f4652c99eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -528,6 +528,7 @@ fun QuickActionAlertDialog( title: String, textContent: String, buttonIconResource: Int, + buttonIconReference: Int, buttonText: String, buttonColors: ButtonColors = ButtonDefaults.buttonColors(), onClickDoOnce: () -> Unit, @@ -539,7 +540,7 @@ fun QuickActionAlertDialog( textContent = textContent, icon = { Icon( - painter = painterRes(buttonIconResource), + painter = painterRes(buttonIconResource, buttonIconReference), contentDescription = null, ) }, @@ -626,6 +627,7 @@ fun QuickActionAlertDialogOneButton( title: String, textContent: String, buttonIconResource: Int, + buttonIconReference: Int, buttonText: String, buttonColors: ButtonColors = ButtonDefaults.buttonColors(), onClickDoOnce: () -> Unit, @@ -636,7 +638,7 @@ fun QuickActionAlertDialogOneButton( textContent = textContent, icon = { Icon( - painter = painterRes(buttonIconResource), + painter = painterRes(buttonIconResource, buttonIconReference), contentDescription = null, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 8f3b353c55..34aa68b64f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -98,6 +98,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer @@ -460,9 +461,9 @@ fun UpdateZapAmountContent( }, ) { Icon( - painter = painterRes(R.drawable.alby), + painter = painterRes(R.drawable.alby, 1), contentDescription = stringRes(id = R.string.accessibility_navigate_to_alby), - modifier = Modifier.size(24.dp), + modifier = Size24Modifier, tint = Color.Unspecified, ) } @@ -475,14 +476,14 @@ fun UpdateZapAmountContent( Icon( Icons.Outlined.ContentPaste, contentDescription = stringRes(id = R.string.paste_from_clipboard), - modifier = Modifier.size(24.dp), + modifier = Size24Modifier, tint = MaterialTheme.colorScheme.primary, ) } IconButton(onClick = { qrScanning = true }) { Icon( - painter = painterRes(R.drawable.ic_qrcode), + painter = painterRes(R.drawable.ic_qrcode, 3), contentDescription = stringRes(id = R.string.accessibility_scan_qr_code), modifier = Modifier.size(24.dp), tint = MaterialTheme.colorScheme.primary, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/PollField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/PollField.kt index 9bc2c30c71..1928d52943 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/PollField.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zappolls/PollField.kt @@ -65,7 +65,7 @@ fun PollField(postViewModel: ShortNotePostViewModel) { ), ) { Image( - painterRes(id = android.R.drawable.ic_input_add), + painter = painterRes(resourceId = android.R.drawable.ic_input_add, 1), contentDescription = "Add poll option button", modifier = Size18Modifier, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt index 61f5fd9bba..6c1427bf14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt @@ -90,7 +90,7 @@ fun BannerImage( accountViewModel = accountViewModel, onError = { Image( - painter = painterRes(R.drawable.profile_banner), + painter = painterRes(R.drawable.profile_banner, 4), contentDescription = stringRes(R.string.profile_banner), contentScale = ContentScale.Crop, modifier = modifier, @@ -99,7 +99,7 @@ fun BannerImage( ) } else { Image( - painter = painterRes(R.drawable.profile_banner), + painter = painterRes(R.drawable.profile_banner, 5), contentDescription = stringRes(R.string.profile_banner), contentScale = ContentScale.Crop, modifier = modifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index fecbeabb1b..1fbdc21c77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -166,7 +166,7 @@ fun GenericCommentPostScreen( onClick = { postViewModel.showRelaysDialog = true }, ) { Icon( - painter = painterRes(R.drawable.relays), + painter = painterRes(R.drawable.relays, 5), contentDescription = stringRes(id = R.string.relay_list_selector), modifier = Modifier.height(25.dp), tint = MaterialTheme.colorScheme.onBackground, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index c09bdf48d0..64533d9a88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -122,7 +122,7 @@ fun RenderAppDefinition( } } else { Image( - painter = painterRes(R.drawable.profile_banner), + painter = painterRes(R.drawable.profile_banner, 6), contentDescription = stringRes(id = R.string.profile_banner), contentScale = ContentScale.FillWidth, modifier = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt index 74f4217b28..a7f7444e8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/IncognitoBadge.kt @@ -27,8 +27,8 @@ import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.painterRes +import com.vitorpamplona.amethyst.ui.theme.IncognitoIconModifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.incognitoIconModifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group @@ -37,17 +37,17 @@ import com.vitorpamplona.quartz.nip17Dm.base.NIP17Group fun IncognitoBadge(baseNote: Note) { if (baseNote.event is NIP17Group) { Icon( - painter = painterRes(id = R.drawable.incognito), + painter = painterRes(resourceId = R.drawable.incognito, 1), null, - modifier = incognitoIconModifier, + modifier = IncognitoIconModifier, tint = MaterialTheme.colorScheme.placeholderText, ) Spacer(modifier = StdHorzSpacer) } else if (baseNote.event is PrivateDmEvent) { Icon( - painter = painterRes(id = R.drawable.incognito_off), + painter = painterRes(resourceId = R.drawable.incognito_off, 1), null, - modifier = incognitoIconModifier, + modifier = IncognitoIconModifier, tint = MaterialTheme.colorScheme.placeholderText, ) Spacer(modifier = StdHorzSpacer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ToggleNip17Button.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ToggleNip17Button.kt index d6a7770476..34912b7c51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ToggleNip17Button.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ToggleNip17Button.kt @@ -20,9 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -34,11 +33,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.note.IncognitoIconOff -import com.vitorpamplona.amethyst.ui.note.IncognitoIconOn import com.vitorpamplona.amethyst.ui.note.QuickActionAlertDialog +import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.IncognitoIconButtonModifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.coroutines.launch @@ -72,19 +71,17 @@ fun ToggleNip17Button( }, ) { if (channelScreenModel.nip17) { - IncognitoIconOn( - modifier = - Modifier - .padding(top = 2.dp) - .size(20.dp), + Icon( + painter = painterRes(R.drawable.incognito, 2), + contentDescription = stringRes(id = R.string.accessibility_turn_off_sealed_message), + modifier = IncognitoIconButtonModifier, tint = MaterialTheme.colorScheme.primary, ) } else { - IncognitoIconOff( - modifier = - Modifier - .padding(top = 2.dp) - .size(20.dp), + Icon( + painter = painterRes(R.drawable.incognito_off, 2), + contentDescription = stringRes(id = R.string.accessibility_turn_on_sealed_message), + modifier = IncognitoIconButtonModifier, tint = MaterialTheme.colorScheme.placeholderText, ) } @@ -103,6 +100,7 @@ fun NewFeatureNIP17AlertDialog( title = stringRes(R.string.new_feature_nip17_might_not_be_available_title), textContent = stringRes(R.string.new_feature_nip17_might_not_be_available_description), buttonIconResource = R.drawable.incognito, + buttonIconReference = 3, buttonText = stringRes(R.string.new_feature_nip17_activate), onClickDoOnce = { scope.launch { onConfirm() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt index 42f7023f00..67ea494ab7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt @@ -20,15 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.communities -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.LoadNote @@ -37,6 +34,7 @@ import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier @Composable @@ -69,9 +67,9 @@ fun NewCommunityNoteButton( containerColor = MaterialTheme.colorScheme.primary, ) { Icon( - painter = painterRes(R.drawable.ic_compose), + painter = painterRes(R.drawable.ic_compose, 2), contentDescription = stringRes(id = R.string.new_community_note), - modifier = Modifier.size(26.dp), + modifier = Size26Modifier, tint = Color.White, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 2332b3f414..580f31de06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -170,7 +170,7 @@ fun NewProductScreen( onClick = { showRelaysDialog = true }, ) { Icon( - painter = painterRes(R.drawable.relays), + painter = painterRes(R.drawable.relays, 1), contentDescription = stringRes(id = R.string.relay_list_selector), modifier = Modifier.height(25.dp), tint = MaterialTheme.colorScheme.onBackground, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt index 45a8f79e86..7a79446286 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt @@ -20,21 +20,19 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier @Composable @@ -52,9 +50,9 @@ fun NewGeoPostButton( containerColor = MaterialTheme.colorScheme.primary, ) { Icon( - painter = painterRes(R.drawable.ic_compose), + painter = painterRes(R.drawable.ic_compose, 1), contentDescription = stringRes(id = R.string.new_community_note), - modifier = Modifier.size(26.dp), + modifier = Size26Modifier, tint = Color.White, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt index fa16b2a52e..77ea685235 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt @@ -20,21 +20,19 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier @Composable @@ -52,9 +50,9 @@ fun NewHashtagPostButton( containerColor = MaterialTheme.colorScheme.primary, ) { Icon( - painter = painterRes(R.drawable.ic_compose), + painter = painterRes(R.drawable.ic_compose, 3), contentDescription = stringRes(id = R.string.new_community_note), - modifier = Modifier.size(26.dp), + modifier = Size26Modifier, tint = Color.White, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt index 6a6a4f59cc..4478b6cf94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt @@ -20,20 +20,18 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier @Composable @@ -47,9 +45,9 @@ fun NewNoteButton(nav: INav) { containerColor = MaterialTheme.colorScheme.primary, ) { Icon( - painter = painterRes(R.drawable.ic_compose), + painter = painterRes(R.drawable.ic_compose, 4), contentDescription = stringRes(R.string.new_post), - modifier = Modifier.size(26.dp), + modifier = Size26Modifier, tint = Color.White, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 356bef90ce..1e22671f59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -71,8 +71,6 @@ import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.Nav import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.note.PollIcon -import com.vitorpamplona.amethyst.ui.note.RegularPostIcon import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer @@ -100,6 +98,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer @@ -195,7 +194,7 @@ private fun NewPostScreenInner( onClick = { postViewModel.showRelaysDialog = true }, ) { Icon( - painter = painterRes(R.drawable.relays), + painter = painterRes(R.drawable.relays, 6), contentDescription = stringRes(id = R.string.relay_list_selector), modifier = Modifier.height(25.dp), tint = MaterialTheme.colorScheme.onBackground, @@ -530,9 +529,19 @@ private fun AddPollButton( onClick = { onClick() }, ) { if (!isPollActive) { - PollIcon() + Icon( + painter = painterRes(R.drawable.ic_poll, 1), + contentDescription = stringRes(id = R.string.poll), + modifier = Size20Modifier, + tint = MaterialTheme.colorScheme.onBackground, + ) } else { - RegularPostIcon() + Icon( + painter = painterRes(R.drawable.ic_lists, 1), + contentDescription = stringRes(id = R.string.disable_poll), + modifier = Size20Modifier, + tint = MaterialTheme.colorScheme.onBackground, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupDialog.kt index a4cf57fc0e..4d6e95a212 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/keyBackup/AccountBackupDialog.kt @@ -503,7 +503,7 @@ private fun QrCodeButtonBase( }, ) { Icon( - painter = painterRes(R.drawable.ic_qrcode), + painter = painterRes(R.drawable.ic_qrcode, 4), contentDescription = stringRes(id = contentDescription), modifier = Modifier.size(24.dp), tint = if (isEnabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.grayText, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 3ae75e8f42..9b5d3f74ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -146,7 +146,7 @@ fun DrawAdditionalInfo( onClick = { dialogOpen = true }, ) { Icon( - painter = painterRes(R.drawable.ic_qrcode), + painter = painterRes(R.drawable.ic_qrcode, 1), contentDescription = stringRes(id = R.string.show_npub_as_a_qr_code), modifier = Size15Modifier, tint = MaterialTheme.colorScheme.placeholderText, @@ -196,7 +196,7 @@ fun DrawAdditionalInfo( Row(verticalAlignment = Alignment.CenterVertically) { Icon( tint = Color.Unspecified, - painter = painterRes(id = getIdentityClaimIcon(identity)), + painter = painterRes(resourceId = getIdentityClaimIcon(identity), getIdentityClaimIconReference(identity)), contentDescription = stringRes(getIdentityClaimDescription(identity)), modifier = Modifier.size(16.dp), ) @@ -253,3 +253,12 @@ fun getIdentityClaimDescription(identity: IdentityClaimTag): Int = is GitHubIdentity -> R.string.github else -> R.drawable.github } + +fun getIdentityClaimIconReference(identity: IdentityClaimTag): Int = + when (identity) { + is TwitterIdentity -> 0 + is TelegramIdentity -> 0 + is MastodonIdentity -> 0 + is GitHubIdentity -> 0 + else -> 0 + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt index c516c8aeae..fd87e10452 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt @@ -70,7 +70,7 @@ fun DrawBanner( model = banner, contentDescription = stringRes(id = R.string.profile_image), contentScale = ContentScale.FillWidth, - placeholder = painterRes(R.drawable.profile_banner), + placeholder = painterRes(R.drawable.profile_banner, 1), modifier = Modifier .fillMaxWidth() @@ -90,7 +90,7 @@ fun DrawBanner( } } else { Image( - painter = painterRes(R.drawable.profile_banner), + painter = painterRes(R.drawable.profile_banner, 2), contentDescription = stringRes(id = R.string.profile_banner), contentScale = ContentScale.FillWidth, modifier = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt index b2b1f36b7d..9f161ddc8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt @@ -59,7 +59,7 @@ fun MessageButton( contentPadding = ZeroPadding, ) { Icon( - painter = painterRes(R.drawable.ic_dm), + painter = painterRes(R.drawable.ic_dm, 1), stringRes(R.string.send_a_direct_message), modifier = Size20Modifier, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt index 773a8877e9..86622eba91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt @@ -36,11 +36,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.WarningColor import com.vitorpamplona.amethyst.ui.theme.allGoodColor @@ -84,7 +84,7 @@ fun RelayNameAndRemoveButton( if (item.forcesTor) { Icon( - painter = painterResource(R.drawable.ic_tor), + painter = painterRes(R.drawable.ic_tor, 2), contentDescription = stringRes(id = R.string.tor_relay), modifier = Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 3f721fbc79..f4a333bdac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -849,7 +849,7 @@ private fun RenderClassifiedsReaderForThread( verticalAlignment = Alignment.CenterVertically, ) { Icon( - painter = painterRes(R.drawable.ic_dm), + painter = painterRes(R.drawable.ic_dm, 5), stringRes(R.string.send_a_direct_message), modifier = Modifier.size(20.dp), tint = MaterialTheme.colorScheme.primary, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt index ce2a49a975..e04d5dd9aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.Size55Modifier import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -178,7 +179,7 @@ fun NewImageButton( Icon( imageVector = Icons.Outlined.Close, contentDescription = stringRes(id = R.string.new_short), - modifier = Modifier.size(26.dp), + modifier = Size26Modifier, tint = Color.White, ) } @@ -189,9 +190,9 @@ fun NewImageButton( exit = fadeOut(), ) { Icon( - painter = painterRes(R.drawable.ic_compose), + painter = painterRes(R.drawable.ic_compose, 5), contentDescription = stringRes(id = R.string.new_short), - modifier = Modifier.size(26.dp), + modifier = Size26Modifier, tint = Color.White, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index a46c60fa67..4b442a304f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -592,7 +592,7 @@ fun KeyTextField( } IconButton(onClick = { dialogOpen = true }) { Icon( - painter = painterRes(R.drawable.ic_qrcode), + painter = painterRes(R.drawable.ic_qrcode, 5), contentDescription = stringRes( R.string.login_with_qr_code, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index b70d522d11..35231bc118 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -284,10 +284,8 @@ val inlinePlaceholder = placeholderVerticalAlign = PlaceholderVerticalAlign.Center, ) -val incognitoIconModifier = - Modifier - .padding(top = 1.dp) - .size(14.dp) +val IncognitoIconModifier = Modifier.padding(top = 1.dp).size(14.dp) +val IncognitoIconButtonModifier = Modifier.padding(top = 2.dp).size(20.dp) val hashVerifierMark = Modifier.width(40.dp).height(40.dp).padding(10.dp) From 7eeb2ed66a7cc244f5b09821e68826aa3dd477b0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 18:26:34 -0400 Subject: [PATCH 034/185] Fixes count call mistake --- .../quartz/nip01Core/relay/client/pool/RelayPool.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt index 76b4d6f8b0..3c96b0e861 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -101,7 +101,7 @@ class RelayPool( relays.forEach { url, relay -> val filters = filters.mapNotNull { it.toFilter(url) } if (filters.isNotEmpty()) { - relay.sendRequest(subId, filters) + relay.sendCount(subId, filters) } } } From 28d182cc55adf81d32b59d7b9dc4048962cff874 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 3 Jul 2025 18:26:57 -0400 Subject: [PATCH 035/185] Updates tor, okhttp and navigation compose --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af738062c6..05bf20797a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,14 +35,14 @@ markdown = "e1151c8" media3 = "1.7.1" mockk = "1.14.4" kotlinx-coroutines-test = "1.10.2" -navigationCompose = "2.9.0" -okhttp = "5.0.0-alpha.17" +navigationCompose = "2.9.1" +okhttp = "5.0.0" runner = "1.6.2" rfc3986 = "0.1.2" secp256k1KmpJniAndroid = "0.18.0" securityCryptoKtx = "1.1.0-beta01" spotless = "7.0.4" -torAndroid = "0.4.8.16.2" +torAndroid = "0.4.8.17" translate = "17.0.3" unifiedpush = "3.0.10" urlDetector = "0.1.23" From cf4851660268996155ca25d41162a56395c815ba Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 7 Jul 2025 13:37:57 -0400 Subject: [PATCH 036/185] Makes sure Filters are not updated if the since is the only change --- .../vitorpamplona/amethyst/model/Account.kt | 6 +- .../service/notifications/RegisterAccounts.kt | 28 ++-- .../ComposeSubscriptionManager.kt | 10 -- .../ComposeSubscriptionManagerControls.kt | 4 - .../MutableComposeSubscriptionManager.kt | 10 -- .../eoseManagers/BaseEoseManager.kt | 34 ++-- .../eoseManagers/PerUniqueIdEoseManager.kt | 4 +- .../PerUserAndFollowListEoseManager.kt | 4 +- .../eoseManagers/PerUserEoseManager.kt | 5 +- .../eoseManagers/SingleSubEoseManager.kt | 4 +- .../SingleSubNoEoseCacheEoseManager.kt | 5 +- .../account/AccountFilterAssembler.kt | 6 +- .../metadata/AccountMetadataEoseManager.kt | 2 +- .../ChannelFinderFilterAssemblyGroup.kt | 4 - .../event/EventFinderFilterAssembler.kt | 4 - .../nwc/NWCPaymentFilterAssembler.kt | 4 - .../user/UserFinderFilterAssembler.kt | 4 - .../searchCommand/SearchFilterAssembler.kt | 4 - .../amethyst/ui/note/RelayCompose.kt | 3 +- .../ui/screen/loggedIn/AccountViewModel.kt | 16 -- .../datasource/ChatroomFilterAssembler.kt | 6 +- .../datasource/ChannelFilterAssembler.kt | 6 +- .../datasource/ChatroomListFilterAssembler.kt | 6 +- .../datasource/CommunityFilterAssembler.kt | 6 +- .../datasource/DiscoveryFilterAssembler.kt | 6 +- .../datasource/GeoHashFilterAssembler.kt | 6 +- .../datasource/HashtagFilterAssembler.kt | 6 +- .../home/datasource/HomeFilterAssembler.kt | 6 +- .../datasource/UserProfileFilterAssembler.kt | 4 - .../datasources/ThreadFilterAssembler.kt | 6 +- .../video/datasource/VideoFilterAssembler.kt | 6 +- .../relays/datasources/Subscription.kt | 60 ++----- .../datasources/SubscriptionController.kt | 153 ++++------------- .../SubscriptionControllerService.kt | 33 ---- .../relays/datasources/SubscriptionSet.kt | 47 ------ .../relays/datasources/SubscriptionStats.kt | 22 ++- .../nip01Core/relay/client/NostrClient.kt | 154 +++++++++++++++--- .../NostrClientSingleDownloadExt.kt | 7 +- .../relay/client/pool/PoolSubscription.kt | 29 ---- .../client/pool/PoolSubscriptionRepository.kt | 38 ++--- .../relay/client/pool/RelayBasedFilter.kt | 8 + .../nip01Core/relay/client/pool/RelayPool.kt | 40 ++++- .../client/single/basic/BasicRelayClient.kt | 2 +- .../vitorpamplona/quartz/utils/LargeCache.kt | 104 +++++++++++- 44 files changed, 397 insertions(+), 525 deletions(-) delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionControllerService.kt delete mode 100644 ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionSet.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscription.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 1a2b26af04..ace9e8406e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -97,7 +97,6 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.downloadFirstEvent -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -953,9 +952,8 @@ class Account( it.host?.let { host -> client.downloadFirstEvent( filters = - note.relays.map { relay -> - RelayBasedFilter( - relay, + note.relays.associateWith { relay -> + listOf( Filter( ids = listOf(host.id), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index bac952a2c4..9a43fb3981 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -96,26 +96,26 @@ class RegisterAccounts( val readyToSend = accounts .mapNotNull { - Log.d(tag, "Register Account ${it.npub}") + if (it.hasPrivKey || it.loggedInWithExternalSigner) { + Log.d(tag, "Register Account ${it.npub}") - val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub) - if (acc != null && acc.isWriteable()) { - val nip65Read = acc.backupNIP65RelayList?.readRelaysNorm() ?: emptyList() + val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub) + if (acc != null && acc.isWriteable()) { + val nip65Read = acc.backupNIP65RelayList?.readRelaysNorm() ?: emptyList() - Log.d(tag, "Register Account ${it.npub} NIP65 Reads ${nip65Read.joinToString(", ") { it.url } }") + Log.d(tag, "Register Account ${it.npub} NIP65 Reads ${nip65Read.joinToString(", ") { it.url } }") - val nip17Read = acc.backupDMRelayList?.relays() ?: emptyList() + val nip17Read = acc.backupDMRelayList?.relays() ?: emptyList() - Log.d(tag, "Register Account ${it.npub} NIP17 Reads ${nip17Read.joinToString(", ") { it.url } }") + Log.d(tag, "Register Account ${it.npub} NIP17 Reads ${nip17Read.joinToString(", ") { it.url } }") - val readKind3Relays = acc.backupContactList?.relays()?.mapNotNull { if (it.value.read) it.key else null } ?: emptyList() + val relays = (nip65Read + nip17Read) - Log.d(tag, "Register Account ${it.npub} Kind3 Reads ${readKind3Relays.joinToString(", ") { it.url } }") - - val relays = (nip65Read + nip17Read + readKind3Relays) - - if (relays.isNotEmpty()) { - Pair(acc, relays) + if (relays.isNotEmpty()) { + Pair(acc, relays) + } else { + null + } } else { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt index 236d469ee9..4c9bb3f66f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt @@ -34,14 +34,8 @@ abstract class ComposeSubscriptionManager : ComposeSubscriptionManagerControl fun subscribe(query: T?) { if (query == null) return - val wasEmpty = composeSubscriptions.isEmpty() - composeSubscriptions.put(query, query) - if (wasEmpty) { - start() - } - invalidateKeys() } @@ -52,10 +46,6 @@ abstract class ComposeSubscriptionManager : ComposeSubscriptionManagerControl composeSubscriptions.remove(query) invalidateKeys() - - if (composeSubscriptions.isEmpty()) { - stop() - } } fun allKeys() = composeSubscriptions.keys diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt index 6a1d54fd19..479bde071d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/ComposeSubscriptionManagerControls.kt @@ -21,10 +21,6 @@ package com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers interface ComposeSubscriptionManagerControls { - fun start() - - fun stop() - fun invalidateKeys() fun invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt index a9520a230c..9966f901d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/composeSubscriptionManagers/MutableComposeSubscriptionManager.kt @@ -45,8 +45,6 @@ abstract class MutableComposeSubscriptionManager( fun subscribe(query: T?) { if (query == null) return - val wasEmpty = composeSubscriptions.isEmpty() - composeSubscriptions[query]?.cancel() composeSubscriptions[query] = scope.launch { @@ -55,10 +53,6 @@ abstract class MutableComposeSubscriptionManager( } } - if (wasEmpty) { - start() - } - invalidateKeys() } @@ -70,10 +64,6 @@ abstract class MutableComposeSubscriptionManager( composeSubscriptions.remove(query) invalidateKeys() - - if (composeSubscriptions.isEmpty()) { - stop() - } } fun allKeys() = composeSubscriptions.keys diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt index fb5fce6461..f90d643bd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt @@ -22,49 +22,41 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import android.util.Log import com.vitorpamplona.amethyst.isDebug +import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.ammolite.relays.datasources.SubscriptionController import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import kotlinx.coroutines.Dispatchers abstract class BaseEoseManager( val client: NostrClient, val allKeys: () -> Set, ) { - val orchestrator = - SubscriptionController(client) { - if (isDebug) Log.d("${this.javaClass.simpleName}", "Updating Subscriptions") - updateSubscriptions(allKeys()) - } + val orchestrator = SubscriptionController(client) abstract fun updateSubscriptions(keys: Set) fun printStats() = orchestrator.printStats(this.javaClass.simpleName) - fun invalidateFilters() = orchestrator.invalidateFilters() + // Refreshes observers in batches. + private val bundler = BundledUpdate(300, Dispatchers.Default) - fun start() { - orchestrator.start() - if (isDebug) { - Log.d("${this.javaClass.simpleName}", "Start") + fun invalidateFilters() { + bundler.invalidate { + forceInvalidate() } } - fun stop() { - orchestrator.stop() - if (isDebug) { - Log.d("${this.javaClass.simpleName}", "Stop") - } + fun forceInvalidate() { + updateSubscriptions(allKeys()) + + orchestrator.updateRelays() } fun destroy() { + bundler.cancel() orchestrator.destroy() if (isDebug) { Log.d("${this.javaClass.simpleName}", "Destroy, Unsubscribe") } } - - fun init() { - if (isDebug) { - Log.d("${this.javaClass.simpleName}", "Init, Subscribe") - } - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt index 8cf1b6df9a..2a9f0169ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlin.collections.distinctBy @@ -90,7 +91,8 @@ abstract class PerUniqueIdEoseManager( uniqueSubscribedAccounts.forEach { val mainKey = id(it) - findOrCreateSubFor(it).relayBasedFilters = updateFilter(it, since(it))?.ifEmpty { null } + val newFilters = updateFilter(it, since(it))?.ifEmpty { null } + findOrCreateSubFor(it).updateFilters(newFilters?.groupByRelay()) updated.add(mainKey) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index 0c0da81ec8..9480606821 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl /** @@ -89,7 +90,8 @@ abstract class PerUserAndFollowListEoseManager( uniqueSubscribedAccounts.forEach { val user = user(it) val sub = findOrCreateSubFor(it) - sub.relayBasedFilters = updateFilter(it, since(it))?.ifEmpty { null } + val newFilters = updateFilter(it, since(it))?.ifEmpty { null } + sub.updateFilters(newFilters?.groupByRelay()) updated.add(user) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index 1854f57d1e..404ab61431 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlin.collections.distinctBy @@ -87,7 +88,9 @@ abstract class PerUserEoseManager( uniqueSubscribedAccounts.forEach { val user = user(it) - findOrCreateSubFor(it).relayBasedFilters = updateFilter(it, since(it))?.ifEmpty { null } + val newFilters = updateFilter(it, since(it))?.ifEmpty { null } + + findOrCreateSubFor(it).updateFilters(newFilters?.groupByRelay()) updated.add(user) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt index 992465e0cc..c11aff6578 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSERelayList import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlin.collections.distinctBy @@ -64,8 +65,9 @@ abstract class SingleSubEoseManager( override fun updateSubscriptions(keys: Set) { val uniqueSubscribedAccounts = keys.distinctBy { distinct(it) } + val newFilters = updateFilter(uniqueSubscribedAccounts, since())?.ifEmpty { null } - sub.relayBasedFilters = updateFilter(uniqueSubscribedAccounts, since())?.ifEmpty { null } + sub.updateFilters(newFilters?.groupByRelay()) } abstract fun updateFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt index 86a72b96d5..90d8d79fa5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import kotlin.collections.distinctBy /** @@ -43,8 +44,8 @@ abstract class SingleSubNoEoseCacheEoseManager( override fun updateSubscriptions(keys: Set) { val uniqueSubscribedAccounts = keys.distinctBy { distinct(it) } - - sub.relayBasedFilters = updateFilter(uniqueSubscribedAccounts)?.ifEmpty { null } + val newFilters = updateFilter(uniqueSubscribedAccounts)?.ifEmpty { null } + sub.updateFilters(newFilters?.groupByRelay()) } abstract fun updateFilter(keys: List): List? diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt index cd4be9b40f..68fea5dac8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt @@ -47,15 +47,11 @@ class AccountFilterAssembler( AccountNotificationsEoseManager(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt index 61f9a7b7b5..044a23d759 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt @@ -38,7 +38,7 @@ class AccountMetadataEoseManager( client: NostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { - override fun user(query: AccountQueryState) = query.account.userProfile() + override fun user(key: AccountQueryState) = key.account.userProfile() fun relayFlow(query: AccountQueryState) = query.account.outboxRelays.flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt index cc794358fa..797d2cccec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt @@ -44,10 +44,6 @@ class ChannelFinderFilterAssemblyGroup( ChannelMetadataAndLiveActivityWatcherSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateFilters() = group.forEach { it.invalidateFilters() } override fun invalidateKeys() = invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt index 0ba866dcfd..beaf7d2511 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt @@ -40,10 +40,6 @@ class EventFinderFilterAssembler( EventWatcherSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateFilters() = group.forEach { it.invalidateFilters() } override fun invalidateKeys() = invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt index 4e4d91572f..71ca89d703 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt @@ -41,10 +41,6 @@ class NWCPaymentFilterAssembler( NWCPaymentWatcherSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateFilters() = group.forEach { it.invalidateFilters() } override fun invalidateKeys() = invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt index b7e6b8a52b..c736b6e45e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt @@ -42,10 +42,6 @@ class UserFinderFilterAssembler( UserWatcherSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateFilters() = group.forEach { it.invalidateFilters() } override fun invalidateKeys() = invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt index caa17059f1..754332de8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/SearchFilterAssembler.kt @@ -49,10 +49,6 @@ class SearchFilterAssembler( SearchWatcherSubAssembler(cache, client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateFilters() = group.forEach { it.invalidateFilters() } override fun invalidateKeys() = invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt index 4fa5b6d89b..45ff9323d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayCompose.kt @@ -109,7 +109,8 @@ private fun RelayOptions( onAddRelay: () -> Unit, onRemoveRelay: () -> Unit, ) { - val userState by accountViewModel.normalizedKind3RelaySetFlow.collectAsStateWithLifecycle() + val userState by accountViewModel.account.trustedRelays.flow + .collectAsStateWithLifecycle() if (!userState.contains(relay.url)) { AddRelayButton(onAddRelay) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 6056e32b11..e45801ec94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -161,22 +161,6 @@ class AccountViewModel( var firstRoute: Route? = null - val normalizedKind3RelaySetFlow: StateFlow> = - account - .userProfile() - .flow() - .relays.stateFlow - .map { contactListState -> - contactListState.user.latestContactList - ?.relays() - ?.keys ?: emptySet() - }.flowOn(Dispatchers.Default) - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(10000, 10000), - emptySet(), - ) - val toastManager = ToastManager() val feedStates = AccountFeedContentStates(this) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt index 2ba8587ca1..740d4cddcd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterAssembler.kt @@ -41,15 +41,11 @@ class ChatroomFilterAssembler( ChatroomFilterSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt index b0faabfc67..bc579d4545 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt @@ -44,15 +44,11 @@ class ChannelFilterAssembler( ChannelFromUserFilterSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt index 4649f1c2bd..fa72099c3c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/ChatroomListFilterAssembler.kt @@ -39,15 +39,11 @@ class ChatroomListFilterAssembler( FollowingEphemeralChatSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt index bc9087f18f..e69a2dad92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFilterAssembler.kt @@ -37,15 +37,11 @@ class CommunityFilterAssembler( CommunityFeedFilterSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt index e820995bcf..b3a9c8f08f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt @@ -41,15 +41,11 @@ class DiscoveryFilterAssembler( DiscoveryFollowsDiscoverySubAssembler3(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt index 088fbe0daa..a1d84cf27a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssembler.kt @@ -45,15 +45,11 @@ class GeoHashFilterAssembler( GeoHashFeedFilterSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt index f21cce3220..ada311d148 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssembler.kt @@ -40,15 +40,11 @@ class HashtagFilterAssembler( HashtagFeedFilterSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt index 06638abdce..43d1cd7704 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt @@ -46,15 +46,11 @@ class HomeFilterAssembler( // MixGeohashHashtagsCommunityEoseManager(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt index be10dce706..f2a5a1d031 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileFilterAssembler.kt @@ -42,10 +42,6 @@ class UserProfileFilterAssembler( UserProfileZapsFilterSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateFilters() = group.forEach { it.invalidateFilters() } override fun invalidateKeys() = invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt index 3e9152ff88..59659205f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt @@ -40,15 +40,11 @@ class ThreadFilterAssembler( ThreadEventLoaderSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt index d1f95d119e..99a3d8f21a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt @@ -40,15 +40,11 @@ class VideoFilterAssembler( VideoOutboxEventsFilterSubAssembler(client, ::allKeys), ) - override fun start() = group.forEach { it.start() } - - override fun stop() = group.forEach { it.stop() } - override fun invalidateKeys() = invalidateFilters() override fun invalidateFilters() = group.forEach { it.invalidateFilters() } - override fun destroy() = group.forEach { it.start() } + override fun destroy() = group.forEach { it.destroy() } override fun printStats() = group.forEach { it.printStats() } } diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt index a5595470f0..364613cdbe 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt @@ -20,70 +20,34 @@ */ package com.vitorpamplona.ammolite.relays.datasources -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlin.collections.forEachIndexed +import kotlin.contracts.ExperimentalContracts data class Subscription( val id: String = newSubId(), val onEose: ((time: Long, relayUrl: NormalizedRelayUrl) -> Unit)? = null, ) { - var relayBasedFilters: List? = null // Inactive when null + private var filters: Map>? = null // Inactive when null fun reset() { - relayBasedFilters = null + filters = null } + fun updateFilters(newFilters: Map>?) { + filters = newFilters + } + + fun filters() = filters + + @OptIn(ExperimentalContracts::class) + fun isActive() = filters != null + fun callEose( time: Long, relay: NormalizedRelayUrl, ) { onEose?.let { it(time, relay) } } - - fun hasChangedFiltersFrom(otherFilters: List?): Boolean { - if (relayBasedFilters == null && otherFilters == null) return false - if (relayBasedFilters?.size != otherFilters?.size) return true - - relayBasedFilters?.forEachIndexed { index, relaySetFilter -> - val otherFilter = otherFilters?.getOrNull(index) ?: return true - - if (relaySetFilter.relay != otherFilter.relay) return true - - return isDifferent(relaySetFilter.filter, otherFilter.filter) - } - return false - } - - fun isDifferent( - filter1: Filter, - filter2: Filter, - ): Boolean { - // Does not check SINCE on purpose. Avoids replacing the filter if SINCE was all that changed. - // fast check - if (filter1.authors?.size != filter2.authors?.size || - filter1.ids?.size != filter2.ids?.size || - filter1.tags?.size != filter2.tags?.size || - filter1.kinds?.size != filter2.kinds?.size || - filter1.limit != filter2.limit || - filter1.search?.length != filter2.search?.length || - filter1.until != filter2.until - ) { - return true - } - - // deep check - if (filter1.ids != filter2.ids || - filter1.authors != filter2.authors || - filter1.tags != filter2.tags || - filter1.kinds != filter2.kinds || - filter1.search != filter2.search - ) { - return true - } - - return false - } } diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt index 5dd295a5dc..d1767f3fa4 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt @@ -20,16 +20,13 @@ */ package com.vitorpamplona.ammolite.relays.datasources -import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers -import java.util.concurrent.atomic.AtomicBoolean +import com.vitorpamplona.quartz.utils.LargeCache /** * Semantically groups Nostr filters and subscriptions in data source objects that @@ -37,13 +34,9 @@ import java.util.concurrent.atomic.AtomicBoolean */ class SubscriptionController( val client: NostrClient, - val updateSubscriptions: () -> Unit, -) : SubscriptionControllerService { - private val subscriptions = SubscriptionSet() - private var active: Boolean = false - private val changingFilters = AtomicBoolean() - - val stats = SubscriptionStats() +) { + private val subscriptions = LargeCache() + private val stats = SubscriptionStats() private val clientListener = object : IRelayClientListener { @@ -54,10 +47,10 @@ class SubscriptionController( arrivalTime: Long, afterEOSE: Boolean, ) { - if (subscriptions.contains(subId)) { + if (subscriptions.containsKey(subId)) { stats.add(subId, event.kind) if (afterEOSE) { - runAfterEOSE(subId, relay, arrivalTime) + subscriptions.get(subId)?.callEose(arrivalTime, relay.url) } } } @@ -67,145 +60,63 @@ class SubscriptionController( subId: String, arrivalTime: Long, ) { - if (subscriptions.contains(subId)) { - runAfterEOSE(subId, relay, arrivalTime) + if (subscriptions.containsKey(subId)) { + subscriptions.get(subId)?.callEose(arrivalTime, relay.url) } } } - private fun runAfterEOSE( - subscriptionId: String, - relay: IRelayClient, - arrivalTime: Long, - ) { - subscriptions[subscriptionId]?.callEose(arrivalTime, relay.url) - } - init { client.subscribe(clientListener) } - override fun destroy() { - // makes sure to run - stop() + fun destroy() { client.unsubscribe(clientListener) - bundler.cancel() } - override fun start() { - active = true - invalidateFilters() - } - - @OptIn(DelicateCoroutinesApi::class) - override fun stop() { - active = false - - subscriptions.forEach { subscription -> - client.close(subscription.id) - subscription.reset() - } - } - - override fun printStats(tag: String) = stats.printCounter(tag) + fun printStats(tag: String) = stats.printCounter(tag) fun getSub(subId: String) = subscriptions.get(subId) - fun requestNewSubscription(onEOSE: ((Long, NormalizedRelayUrl) -> Unit)? = null): Subscription = subscriptions.newSub(onEOSE) + fun requestNewSubscription(onEOSE: ((Long, NormalizedRelayUrl) -> Unit)? = null): Subscription = Subscription(onEose = onEOSE).also { subscriptions.put(it.id, it) } fun dismissSubscription(subId: String) = getSub(subId)?.let { dismissSubscription(it) } fun dismissSubscription(subscription: Subscription) { client.close(subscription.id) subscription.reset() - subscriptions.remove(subscription) + subscriptions.remove(subscription.id) } - fun isUpdatingFilters() = changingFilters.get() - - // Refreshes observers in batches. - private val bundler = BundledUpdate(300, Dispatchers.Default) - - override fun invalidateFilters() { - bundler.invalidate { - // println("DataSource: ${this.javaClass.simpleName} InvalidateFilters") - - // adds the time to perform the refresh into this delay - // holding off new updates in case of heavy refresh routines. - resetFiltersSuspend() - } - } - - private fun resetFiltersSuspend() { - // only runs one at a time. Ignores the others - if (changingFilters.compareAndSet(false, true)) { - try { - resetFiltersSuspendInner() - } finally { - changingFilters.getAndSet(false) + fun updateRelays() { + val currentFilters = + subscriptions.associateWith { id, sub -> + client.getSubscriptionFiltersOrNull(id) } - } - } - private fun resetFiltersSuspendInner() { - // saves the channels that are currently active - val activeSubscriptions = subscriptions.actives() - // saves the current content to only update if it changes - val currentFilters = activeSubscriptions.associate { it.id to client.getSubscriptionFiltersOrNull(it.id) } - - // updates all filters - updateSubscriptions() - - // Makes sure to only send an updated filter when it actually changes. - subscriptions.forEach { newSubscriptionFilters -> - val currentFilters = currentFilters[newSubscriptionFilters.id] - updateRelaysIfNeeded(newSubscriptionFilters, currentFilters) + subscriptions.forEach { id, sub -> + updateRelaysIfNeeded(id, sub.filters(), currentFilters[id]) } } fun updateRelaysIfNeeded( - updatedSubscription: Subscription, - currentFilters: List?, + subId: String, + updatedFilters: Map>?, + currentFilters: Map>?, ) { - val updatedSubscriptionNewFilters = updatedSubscription.relayBasedFilters - - val isActive = client.isActive(updatedSubscription.id) - - if (!isActive && updatedSubscriptionNewFilters != null) { - // Filter was removed from the active list - // but it is supposed to be there. Send again. - if (active) { - client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) + if (currentFilters != null) { + if (updatedFilters == null) { + // was active and is not active anymore, just close. + client.close(subId) + } else { + client.sendRequest(subId, updatedFilters) } } else { - if (currentFilters != null) { - if (updatedSubscriptionNewFilters == null) { - // was active and is not active anymore, just close. - client.close(updatedSubscription.id) - } else { - // was active and is still active, check if it has changed. - if (updatedSubscription.hasChangedFiltersFrom(currentFilters)) { - client.close(updatedSubscription.id) - if (active) { - client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) - } - } else { - // hasn't changed, does nothing. - // unless the relay has disconnected, then reconnect. - if (active) { - client.sendFilterOnlyIfDisconnected(updatedSubscription.id, updatedSubscriptionNewFilters) - } - } - } + if (updatedFilters == null) { + // was not active and is still not active, does nothing } else { - if (updatedSubscriptionNewFilters == null) { - // was not active and is still not active, does nothing - } else { - // was not active and becomes active, sends the filter. - if (active) { - client.sendFilter(updatedSubscription.id, updatedSubscriptionNewFilters) - } - } + // was not active and becomes active, sends the entire filter. + client.sendRequest(subId, updatedFilters) } } } diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionControllerService.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionControllerService.kt deleted file mode 100644 index 0f422a7329..0000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionControllerService.kt +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.relays.datasources - -interface SubscriptionControllerService { - fun start() - - fun stop() - - fun invalidateFilters() - - fun destroy() - - fun printStats(tag: String) -} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionSet.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionSet.kt deleted file mode 100644 index c40e3a42a2..0000000000 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionSet.kt +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.ammolite.relays.datasources - -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl - -class SubscriptionSet { - private var subscriptions = mapOf() - - fun contains(subId: String) = subscriptions.containsKey(subId) - - fun add(sub: Subscription) { - subscriptions = subscriptions + Pair(sub.id, sub) - } - - fun remove(subId: String) { - subscriptions = subscriptions.minus(subId) - } - - fun remove(sub: Subscription) = remove(sub.id) - - fun newSub(onEOSE: ((Long, NormalizedRelayUrl) -> Unit)? = null): Subscription = Subscription(onEose = onEOSE).also { add(it) } - - fun forEach(action: (Subscription) -> Unit) = subscriptions.values.forEach(action) - - operator fun get(subId: String) = subscriptions[subId] - - fun actives() = subscriptions.values.filter { it.relayBasedFilters != null } -} diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionStats.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionStats.kt index 554a30cf1e..03e3330507 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionStats.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionStats.kt @@ -21,17 +21,19 @@ package com.vitorpamplona.ammolite.relays.datasources import android.util.Log +import com.vitorpamplona.quartz.utils.LargeCache class SubscriptionStats { data class Counter( val subscriptionId: String, val eventKind: Int, - var counter: Int, - ) + ) { + var counter: Int = 0 + } - private var eventCounter = mapOf() + private var eventCounter = LargeCache() - fun eventCounterIndex( + private fun eventCounterIndex( str1: String, str2: Int, ): Int = 31 * str1.hashCode() + str2.hashCode() @@ -41,19 +43,15 @@ class SubscriptionStats { eventKind: Int, ) { val key = eventCounterIndex(subscriptionId, eventKind) - val keyValue = eventCounter[key] - if (keyValue != null) { - keyValue.counter++ - } else { - eventCounter = eventCounter + Pair(key, Counter(subscriptionId, eventKind, 1)) - } + val stats = eventCounter.getOrCreate(key) { Counter(subscriptionId, eventKind) } + stats.counter++ } fun printCounter(tag: String) { - eventCounter.forEach { + eventCounter.forEach { _, stats -> Log.d( tag, - "Received Events ${it.value.subscriptionId} ${it.value.eventKind}: ${it.value.counter}", + "Received Events ${stats.subscriptionId} ${stats.eventKind}: ${stats.counter}", ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index 56dcf79554..ea512b6caa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -25,12 +25,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientLis import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.PoolEventOutboxRepository import com.vitorpamplona.quartz.nip01Core.relay.client.pool.PoolSubscriptionRepository -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import kotlinx.coroutines.CoroutineScope @@ -44,14 +44,15 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** - * The Nostr Client manages a relay pool, keeps active subscriptions and manages sending of events. + * The Nostr Client manages a relay pool, keeps active subscriptions and manages re-sending of events. */ class NostrClient( private val websocketBuilder: WebsocketBuilder, private val scope: CoroutineScope, ) : IRelayClientListener { private val relayPool: RelayPool = RelayPool(this, ::buildRelay) - private val activeSubscriptions: PoolSubscriptionRepository = PoolSubscriptionRepository() + private val activeRequests: PoolSubscriptionRepository = PoolSubscriptionRepository() + private val activeCounts: PoolSubscriptionRepository = PoolSubscriptionRepository() private val eventOutbox: PoolEventOutboxRepository = PoolEventOutboxRepository() private var listeners = setOf() @@ -62,19 +63,20 @@ class NostrClient( */ private val allRelays = combine( - activeSubscriptions.relays, + activeRequests.relays, + activeCounts.relays, eventOutbox.relays, - ) { subs, outbox -> - subs + outbox + ) { reqs, counts, outbox -> + reqs + counts + outbox }.onStart { - activeSubscriptions.relays.value + eventOutbox.relays.value + activeRequests.relays.value + activeCounts.relays.value + eventOutbox.relays.value }.onEach { relayPool.updatePool(it) }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Companion.Eagerly, - activeSubscriptions.relays.value + eventOutbox.relays.value, + activeRequests.relays.value + activeCounts.relays.value + eventOutbox.relays.value, ) fun buildRelay(relay: NormalizedRelayUrl): IRelayClient = @@ -84,7 +86,8 @@ class NostrClient( listener = relayPool, stats = RelayStats.get(relay), ) { liveRelay -> - activeSubscriptions.forEachSub(relay, liveRelay::sendRequest) + activeRequests.forEachSub(relay, liveRelay::sendRequest) + activeCounts.forEachSub(relay, liveRelay::sendCount) eventOutbox.forEachUnsentEvent(relay, liveRelay::send) } @@ -109,20 +112,124 @@ class NostrClient( } } - fun sendFilter( - subscriptionId: String = newSubId(), - filters: List = listOf(), - ) { - activeSubscriptions.addOrUpdate(subscriptionId, filters) - relayPool.sendRequest(subscriptionId, filters) + fun needsToResendRequest( + oldFilters: List, + newFilters: List, + ): Boolean { + if (oldFilters.size != newFilters.size) return true + + oldFilters.forEachIndexed { index, oldFilter -> + val newFilter = newFilters.getOrNull(index) ?: return true + + return needsToResendRequest(oldFilter, newFilter) + } + return false } - fun sendFilterOnlyIfDisconnected( - subscriptionId: String = newSubId(), - filters: List = listOf(), + /** + * Checks if the filter has changed, with a special case for when the since changes due to new + * EOSE times. + */ + fun needsToResendRequest( + oldFilter: Filter, + newFilter: Filter, + ): Boolean { + // Does not check SINCE on purpose. Avoids replacing the filter if SINCE was all that changed. + // fast check + if (oldFilter.authors?.size != newFilter.authors?.size || + oldFilter.ids?.size != newFilter.ids?.size || + oldFilter.tags?.size != newFilter.tags?.size || + oldFilter.kinds?.size != newFilter.kinds?.size || + oldFilter.limit != newFilter.limit || + oldFilter.search?.length != newFilter.search?.length || + oldFilter.until != newFilter.until + ) { + return true + } + + // deep check + if (oldFilter.ids != newFilter.ids || + oldFilter.authors != newFilter.authors || + oldFilter.tags != newFilter.tags || + oldFilter.kinds != newFilter.kinds || + oldFilter.search != newFilter.search + ) { + return true + } + + if (oldFilter.since != null) { + if (newFilter.since == null) { + // went was checking the future only and now wants everything + return true + } else if (oldFilter.since > newFilter.since) { + // went backwards in time, forces update + return true + } + } + + return false + } + + fun sendRequest( + subId: String = newSubId(), + filters: Map>, ) { - activeSubscriptions.addOrUpdate(subscriptionId, filters) - relayPool.connectIfDisconnected() + val oldFilters = activeRequests.getSubscriptionFiltersOrNull(subId) ?: emptyMap() + activeRequests.addOrUpdate(subId, filters) + + val allRelays = filters.keys + oldFilters.keys + + allRelays.forEach { relay -> + val oldFilters = oldFilters[relay] + val newFilters = filters[relay] + + if (newFilters.isNullOrEmpty()) { + // some relays are not in this sub anymore. Stop their subscriptions + relayPool.close(relay, subId) + } else if (oldFilters.isNullOrEmpty()) { + // new relays were added. Start a new sub in them + relayPool.sendRequest(relay, subId, newFilters) + } else if (needsToResendRequest(oldFilters, newFilters)) { + // filters were changed enough (not only an update in since) to warn a new update + relayPool.sendRequest(relay, subId, newFilters) + } else { + // makes sure the relay wakes up if it was disconnected by the server + // upon connection, the relay will run the default Sync and update all + // filters, including this one. + relayPool.connectIfDisconnected(relay) + } + } + } + + fun sendCount( + subId: String = newSubId(), + filters: Map>, + ) { + val oldFilters = activeCounts.getSubscriptionFiltersOrNull(subId) ?: emptyMap() + activeCounts.addOrUpdate(subId, filters) + + val allRelays = filters.keys + oldFilters.keys + + allRelays.forEach { relay -> + val oldFilters = oldFilters[relay] + val newFilters = filters[relay] + + if (newFilters.isNullOrEmpty()) { + // some relays are not in this sub anymore. Stop their subscriptions + relayPool.close(relay, subId) + } else if (oldFilters.isNullOrEmpty()) { + // new relays were added. Start a new sub in them + relayPool.sendCount(relay, subId, newFilters) + } else if (needsToResendRequest(oldFilters, newFilters)) { + // filters were changed enough (not only an update in since) to warn a new update + relayPool.sendCount(relay, subId, newFilters) + } else { + // makes sure the relay wakes up if it was disconnected by the server + // upon connection, the relay will run the default Sync and update all + // filters, including this one. + relayPool.connectIfDisconnected(relay) + } + } } fun sendIfExists( @@ -142,11 +249,10 @@ class NostrClient( fun close(subscriptionId: String) { relayPool.close(subscriptionId) - activeSubscriptions.remove(subscriptionId) + activeRequests.remove(subscriptionId) + activeCounts.remove(subscriptionId) } - fun isActive(subscriptionId: String): Boolean = activeSubscriptions.isActive(subscriptionId) - override fun onEvent( relay: IRelayClient, subId: String, @@ -236,7 +342,7 @@ class NostrClient( listeners = listeners.minus(listener) } - fun getSubscriptionFiltersOrNull(subId: String): List? = activeSubscriptions.getSubscriptionFiltersOrNull(subId) + fun getSubscriptionFiltersOrNull(subId: String): Map>? = activeRequests.getSubscriptionFiltersOrNull(subId) fun relayStatusFlow() = relayPool.statusFlow } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt index 29fff89413..dbee706e9d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt @@ -23,9 +23,10 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.acessories import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener -import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay @@ -33,7 +34,7 @@ import kotlinx.coroutines.launch fun NostrClient.downloadFirstEvent( subscriptionId: String = newSubId(), - filters: List = listOf(), + filters: Map>, onResponse: (Event) -> Unit, ) { val listener = @@ -56,7 +57,7 @@ fun NostrClient.downloadFirstEvent( subscribe(listener) - sendFilter(subscriptionId, filters) + sendRequest(subscriptionId, filters) GlobalScope.launch(Dispatchers.IO) { delay(30000) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscription.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscription.kt deleted file mode 100644 index 1f48aa7ccf..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscription.kt +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip01Core.relay.client.pool - -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl - -class PoolSubscription( - var filters: List = emptyList(), -) { - fun toFilter(relay: NormalizedRelayUrl) = filters.mapNotNull { it.toFilter(relay) } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscriptionRepository.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscriptionRepository.kt index 19a0aa4044..c24064a12b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscriptionRepository.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolSubscriptionRepository.kt @@ -22,20 +22,17 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.pool import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.LargeCache import kotlinx.coroutines.flow.MutableStateFlow class PoolSubscriptionRepository { - private var subscriptions = mapOf() + private var subscriptions = LargeCache>>() val relays = MutableStateFlow(setOf()) fun updateRelays() { val myRelays = mutableSetOf() - subscriptions.values.forEach { - it.filters.forEach { - if (!myRelays.contains(it.relay)) { - myRelays.add(it.relay) - } - } + subscriptions.forEach { sub, perRelayFilters -> + myRelays.addAll(perRelayFilters.keys) } if (relays.value != myRelays) { @@ -45,20 +42,15 @@ class PoolSubscriptionRepository { fun addOrUpdate( subscriptionId: String, - filters: List = listOf(), + filters: Map>, ) { - val currentFilter = subscriptions[subscriptionId] - if (currentFilter == null) { - subscriptions = subscriptions + Pair(subscriptionId, PoolSubscription(filters)) - } else { - currentFilter.filters = filters - } + subscriptions.put(subscriptionId, filters) updateRelays() } fun remove(subscriptionId: String) { - if (subscriptions.contains(subscriptionId)) { - subscriptions = subscriptions.minus(subscriptionId) + if (subscriptions.containsKey(subscriptionId)) { + subscriptions.remove(subscriptionId) updateRelays() } } @@ -67,9 +59,9 @@ class PoolSubscriptionRepository { relay: NormalizedRelayUrl, run: (String, List) -> Unit, ) { - subscriptions.forEach { (subId, filters) -> - val filters = filters.toFilter(relay) - if (filters.isNotEmpty()) { + subscriptions.forEach { subId, filters -> + val filters = filters[relay] + if (!filters.isNullOrEmpty()) { run(subId, filters) } else { null @@ -77,11 +69,5 @@ class PoolSubscriptionRepository { } } - fun isActive(subscriptionId: String): Boolean = subscriptions.contains(subscriptionId) - - fun allSubscriptions(): Map = subscriptions - - fun getSubscriptionFilters(subId: String): List = subscriptions[subId]?.filters ?: emptyList() - - fun getSubscriptionFiltersOrNull(subId: String): List? = subscriptions[subId]?.filters + fun getSubscriptionFiltersOrNull(subId: String): Map>? = subscriptions.get(subId) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt index 2383936e74..b5ebfe3583 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt @@ -40,3 +40,11 @@ class RelayBasedFilter( null } } + +fun List.groupByRelay(): Map> { + val result = mutableMapOf>() + for (relayBasedFilter in this) { + result.getOrPut(relayBasedFilter.relay) { mutableListOf() }.add(relayBasedFilter.filter) + } + return result +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt index 3c96b0e861..e179548407 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientList import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.LargeCache import kotlinx.coroutines.flow.MutableStateFlow @@ -33,7 +34,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlin.collections.forEach import kotlin.collections.isNotEmpty -import kotlin.collections.mapNotNull +import kotlin.collections.isNullOrEmpty val UnsupportedRelayCreation: (url: NormalizedRelayUrl) -> IRelayClient = { throw UnsupportedOperationException("Cannot create new relays") @@ -77,30 +78,48 @@ class RelayPool( relay.connectAndSyncFiltersIfDisconnected() } + fun connectIfDisconnected(relay: NormalizedRelayUrl) = relays.get(relay)?.connectAndSyncFiltersIfDisconnected() + fun disconnect() = relays.forEach { url, relay -> relay.disconnect() } fun sendRequest( + relay: NormalizedRelayUrl, subId: String, - filters: List, + filters: List, + ) { + relays.get(relay)?.sendRequest(subId, filters) + } + + fun sendRequest( + subId: String, + filters: Map>, ) { relays.forEach { url, relay -> - val filters = filters.mapNotNull { it.toFilter(url) } - if (filters.isNotEmpty()) { + val filters = filters[relay.url] + if (!filters.isNullOrEmpty()) { relay.sendRequest(subId, filters) } } } - fun sendCounter( + fun sendCount( + relay: NormalizedRelayUrl, subId: String, - filters: List, + filters: List, + ) { + relays.get(relay)?.sendRequest(subId, filters) + } + + fun sendCount( + subId: String, + filters: Map>, ) { relays.forEach { url, relay -> - val filters = filters.mapNotNull { it.toFilter(url) } - if (filters.isNotEmpty()) { + val filters = filters[relay.url] + if (!filters.isNullOrEmpty()) { relay.sendCount(subId, filters) } } @@ -111,6 +130,11 @@ class RelayPool( relay.close(subscriptionId) } + fun close( + relay: NormalizedRelayUrl, + subscriptionId: String, + ) = relays.get(relay)?.close(subscriptionId) + fun send( signedEvent: Event, list: Set, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 7e48fac953..80bb399f45 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -293,7 +293,7 @@ open class BasicRelayClient( } private fun processClosed(msg: ClosedMessage) { - // Log.w(logTag, "Relay Closed Subscription $newMessage") + Log.w(logTag, "Relay Closed Subscription ${msg.subscriptionId} ${msg.message}") afterEOSEPerSubscription[msg.subscriptionId] = false listener.onClosed(this@BasicRelayClient, msg.subscriptionId, msg.message) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt index 65d5cff60d..e2678d9f0b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt @@ -20,8 +20,10 @@ */ package com.vitorpamplona.quartz.utils +import android.R.attr.value import java.util.concurrent.ConcurrentSkipListMap import java.util.function.BiConsumer +import kotlin.collections.LinkedHashMap class LargeCache { private val cache = ConcurrentSkipListMap() @@ -170,14 +172,32 @@ class LargeCache { return runner.count } - private fun innerForEach(runner: BiConsumer) { - // val (value, elapsed) = - // measureTimedValue { - cache.forEach(runner) - // } - // println("LargeCache full loop $elapsed \t for $runner") + public fun associate(transform: (K, V) -> Pair): Map { + val runner = BiAssociateCollector(size(), transform) + innerForEach(runner) + return runner.results + } - listOf(1, 2, 3).joinToString() + public fun associateNotNull(transform: (K, V) -> Pair?): Map { + val runner = BiAssociateNotNullCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + public fun associateWith(transform: (K, V) -> U?): Map { + val runner = BiAssociateWithCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + public fun associateNotNullWith(transform: (K, V) -> U): Map { + val runner = BiAssociateNotNullWithCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + private fun innerForEach(runner: BiConsumer) { + cache.forEach(runner) } fun joinToString( @@ -255,6 +275,13 @@ fun interface BiMapper { ): R? } +fun interface BiMapperNotNull { + fun map( + k: K, + v: V, + ): R +} + class BiMapCollector( val mapper: BiMapper, ) : BiConsumer { @@ -271,6 +298,69 @@ class BiMapCollector( } } +class BiAssociateCollector( + val size: Int, + val mapper: BiMapperNotNull>, +) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val pair = mapper.map(k, v) + results.put(pair.first, pair.second) + } +} + +class BiAssociateNotNullCollector( + val size: Int, + val mapper: BiMapper?>, +) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val pair = mapper.map(k, v) + if (pair != null) { + results.put(pair.first, pair.second) + } + } +} + +class BiAssociateWithCollector( + val size: Int, + val mapper: BiMapper, +) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + results.put(k, mapper.map(k, v)) + } +} + +class BiAssociateNotNullWithCollector( + val size: Int, + val mapper: BiMapper, +) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val newValue = mapper.map(k, v) + if (newValue != null) { + results.put(k, newValue) + } + } +} + class BiMapUniqueCollector( val mapper: BiMapper, ) : BiConsumer { From eb5d11329854fd478fa38d74216ea76acf30e5b5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 7 Jul 2025 13:48:39 -0400 Subject: [PATCH 037/185] new map of set api --- .../event/loaders/FilterMissingEvents.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt index fb8cc330ac..ced79d5a0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt @@ -22,12 +22,12 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.LocalCache.relayHints import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.utils.MapOfSetBuilder +import com.vitorpamplona.quartz.utils.mapOfSet fun filterMissingEvents(keys: List): List? { val missingEvents = mutableSetOf() @@ -51,15 +51,16 @@ fun filterMissingEvents(keys: List): List): List { if (missingEventIds.isEmpty()) return emptyList() - val relayHints = MapOfSetBuilder() - - missingEventIds.forEach { eventId -> - LocalCache.relayHints.hintsForEvent(eventId).forEach { relayUrl -> - relayHints.add(relayUrl, eventId) + val relayHints = + mapOfSet { + missingEventIds.forEach { eventId -> + LocalCache.relayHints.hintsForEvent(eventId).forEach { relayUrl -> + add(relayUrl, eventId) + } + } } - } - return relayHints.build().map { + return relayHints.map { RelayBasedFilter( relay = it.key, filter = Filter(ids = it.value.sorted()), From ed2d7f57c569a7935f6dca1d549f373c275f98f9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 7 Jul 2025 17:02:42 -0400 Subject: [PATCH 038/185] Adds the incoming relay as a hint to all related keys, addresses and event ids. --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/LocalCache.kt | 36 ++++++- .../SearchWatcherSubAssembler.kt | 4 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 4 +- .../amethyst/ui/actions/NewMessageTagger.kt | 4 +- .../amethyst/ui/components/ClickableRoute.kt | 2 +- .../markdown/MarkdownMediaRenderer.kt | 2 +- .../ui/screen/AccountStateViewModel.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 2 +- .../amethyst/NewMessageTaggerKeyParseTest.kt | 22 ++--- .../experimental/zapPolls/PollNoteEvent.kt | 72 +++++++++++++- .../quartz/nip01Core/hints/HintProviders.kt | 7 ++ .../client/single/basic/BasicRelayClient.kt | 2 +- .../quartz/nip01Core/tags/people/PTag.kt | 6 +- .../nip02FollowList/ContactListEvent.kt | 7 ++ .../quartz/nip03Timestamp/OtsEvent.kt | 2 + .../quartz/nip09Deletions/DeletionEvent.kt | 4 + .../quartz/nip10Notes/BaseThreadedEvent.kt | 46 ++++----- .../quartz/nip10Notes/TextNoteEvent.kt | 72 +++++++++++++- .../quartz/nip10Notes/tags/MarkedETag.kt | 2 +- .../quartz/nip17Dm/base/BaseDMGroupEvent.kt | 2 + .../quartz/nip18Reposts/GenericRepostEvent.kt | 6 ++ .../quartz/nip18Reposts/RepostEvent.kt | 7 ++ .../quartz/nip18Reposts/quotes/EntityExt.kt | 7 +- .../quartz/nip18Reposts/quotes/QTag.kt | 18 +++- .../nip18Reposts/quotes/TagArrayBuilderExt.kt | 4 +- .../quartz/nip18Reposts/quotes/TagArrayExt.kt | 2 +- .../quartz/nip19Bech32/ListEntityExt.kt | 99 +++++++++++++++++++ .../quartz/nip19Bech32/Nip19Parser.kt | 10 +- .../entities/{Note.kt => NNote.kt} | 8 +- .../quartz/nip22Comments/CommentEvent.kt | 65 +++++++++++- .../nip23LongContent/LongTextNoteEvent.kt | 52 +++++++++- .../quartz/nip25Reactions/ReactionEvent.kt | 6 ++ .../admin/ChannelCreateEvent.kt | 2 + .../admin/ChannelHideMessageEvent.kt | 2 + .../admin/ChannelMetadataEvent.kt | 7 +- .../admin/ChannelMuteUserEvent.kt | 2 + .../message/ChannelMessageEvent.kt | 72 +++++++++++++- .../selection/EmojiPackSelectionEvent.kt | 2 + .../quartz/nip34Git/issue/GitIssueEvent.kt | 54 +++++++++- .../quartz/nip34Git/patch/GitPatchEvent.kt | 8 ++ .../quartz/nip34Git/reply/GitReplyEvent.kt | 57 ++++++++++- .../nip35Torrents/TorrentCommentEvent.kt | 54 +++++++++- .../quartz/nip37Drafts/DraftEvent.kt | 8 +- .../quartz/nip51Lists/GeneralListEvent.kt | 6 ++ .../chat/LiveActivitiesChatMessageEvent.kt | 55 ++++++++++- .../streaming/LiveActivitiesEvent.kt | 7 ++ .../quartz/nip54Wiki/WikiNoteEvent.kt | 66 ++++++++++++- .../quartz/nip57Zaps/LnZapEvent.kt | 8 +- .../quartz/nip57Zaps/LnZapPrivateEvent.kt | 8 +- .../quartz/nip57Zaps/LnZapRequestEvent.kt | 8 +- .../quartz/nip58Badges/BadgeAwardEvent.kt | 8 +- .../quartz/nip58Badges/BadgeProfilesEvent.kt | 8 +- .../approval/CommunityPostApprovalEvent.kt | 8 ++ .../definition/CommunityDefinitionEvent.kt | 10 +- .../definition/tags/ModeratorTag.kt | 3 +- .../quartz/nip75ZapGoals/GoalEvent.kt | 8 +- .../quartz/nip84Highlights/HighlightEvent.kt | 55 ++++++++++- .../recommendation/AppRecommendationEvent.kt | 2 + .../quartz/nip19Bech32/NIP19ParserTest.kt | 4 +- 60 files changed, 1001 insertions(+), 117 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ListEntityExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/{Note.kt => NNote.kt} (92%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ace9e8406e..f52cf5521c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1462,7 +1462,7 @@ class Account( when (it) { is NPub -> null is NProfile -> null - is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> cache.getOrCreateNote(it.hex) + is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> cache.getOrCreateNote(it.hex) is NEvent -> cache.getOrCreateNote(it.hex) is NEmbed -> cache.getOrCreateNote(it.event.id) is NAddress -> cache.checkGetOrCreateAddressableNote(it.aTag()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 89e2c535d5..c90eac1383 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -191,8 +191,6 @@ import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.concurrent.ConcurrentHashMap -import kotlin.compareTo -import kotlin.text.get interface ILocalCache { fun markAsSeen( @@ -2912,7 +2910,7 @@ object LocalCache : ILocalCache { } getOrCreateUser(nip19.hex) } - is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> { + is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> { getOrCreateNote(nip19.hex) } is NEvent -> { @@ -2976,13 +2974,43 @@ object LocalCache : ILocalCache { ): Boolean { val wasNew = justConsumeInnerInner(event, relay, wasVerified) - if (wasNew && relay != null) { + if (wasNew) { updateHintIndexes(event) } + if (relay != null) { + addIncomingRelayAsHintToAllRelatedEvents(event, relay) + } + return wasNew } + fun addIncomingRelayAsHintToAllRelatedEvents( + event: Event, + relay: NormalizedRelayUrl, + ) { + relayHints.addEvent(event.id, relay) + if (event is AddressableEvent) { + relayHints.addAddress(event.addressTag(), relay) + } + + if (event is EventHintProvider) { + event.linkedEventIds().forEach { + relayHints.addEvent(it, relay) + } + } + if (event is AddressHintProvider) { + event.linkedAddressIds().forEach { + relayHints.addAddress(it, relay) + } + } + if (event is PubKeyHintProvider) { + event.linkedPubKeys().forEach { + relayHints.addKey(it, relay) + } + } + } + fun updateHintIndexes(event: Event) { if (event is EventHintProvider) { event.eventHints().forEach { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt index 0b9657cef2..39b58880b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt @@ -31,11 +31,11 @@ import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec -import com.vitorpamplona.quartz.nip19Bech32.entities.Note import com.vitorpamplona.quartz.utils.Hex /** @@ -69,7 +69,7 @@ class SearchWatcherSubAssembler( is NSec -> filterByAuthor(parsed.toPubKeyHex()) is NPub -> filterByAuthor(parsed.hex) is NProfile -> filterByAuthor(parsed.hex) - is Note -> filterByEvent(parsed.hex) + is NNote -> filterByEvent(parsed.hex) is NEvent -> filterByEvent(parsed.hex) is NEmbed -> emptyList() is NRelay -> emptyList() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 3b5f46db8b..50fb1a403b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -49,9 +49,9 @@ import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip19Bech32.entities.Note import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @@ -163,7 +163,7 @@ fun uriToRoute( when (nip19) { is NPub -> Route.Profile(nip19.hex) is NProfile -> Route.Profile(nip19.hex) - is Note -> Route.Note(nip19.hex) + is NNote -> Route.Note(nip19.hex) is NEvent -> { if (nip19.kind == PrivateDmEvent.KIND) { nip19.author?.let { Route.RoomByAuthor(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index f9d0feeb57..4af0330a5f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -73,7 +73,7 @@ class NewMessageTagger( is NPub -> addUserToMentions(dao.getOrCreateUser(entity.hex)) is NProfile -> addUserToMentions(dao.getOrCreateUser(entity.hex)) - is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> addNoteToReplyTos(dao.getOrCreateNote(entity.hex)) + is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> addNoteToReplyTos(dao.getOrCreateNote(entity.hex)) is NEvent -> addNoteToReplyTos(dao.getOrCreateNote(entity.hex)) is NEmbed -> addNoteToReplyTos(dao.getOrCreateNote(entity.event.id)) @@ -107,7 +107,7 @@ class NewMessageTagger( getNostrAddress(dao.getOrCreateUser(entity.hex).toNProfile(), results.restOfWord) } - is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> { + is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> { getNostrAddress(dao.getOrCreateNote(entity.hex).toNEvent(), results.restOfWord) } is NEvent -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 047ab7398a..ce79850d6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -95,7 +95,7 @@ fun ClickableRoute( when (val entity = nip19.entity) { is NPub -> DisplayUser(entity.hex, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) is NProfile -> DisplayUser(entity.hex, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) - is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> DisplayEvent(entity.hex, null, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) + is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> DisplayEvent(entity.hex, null, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) is NEvent -> DisplayEvent(entity.hex, entity.kind, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) is NEmbed -> LoadAndDisplayEvent(entity.event, nip19.additionalChars, accountViewModel, nav) is NAddress -> DisplayAddress(entity, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt index e5784f8170..9260f1b585 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt @@ -171,7 +171,7 @@ class MarkdownMediaRenderer( when (val entity = loadedLink.nip19.entity) { is NPub -> renderObservableUser(entity.hex, loadedLink.nip19.nip19raw, richTextStringBuilder) is NProfile -> renderObservableUser(entity.hex, loadedLink.nip19.nip19raw, richTextStringBuilder) - is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder) + is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder) is NEvent -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder) is NEmbed -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder) is NAddress -> renderObservableShortNoteUri(loadedLink, uri, richTextStringBuilder) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 43a832f8fe..101800e4d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -116,7 +116,7 @@ class AccountStateViewModel : ViewModel() { is NSec -> null is NPub -> parsed.hex.hexToByteArray() is NProfile -> parsed.hex.hexToByteArray() - is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> null + is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> null is NEvent -> null is NEmbed -> null is NRelay -> null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index e45801ec94..9985fc7cd4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1705,7 +1705,7 @@ class AccountViewModel( is NSec -> {} is NPub -> {} is NProfile -> {} - is com.vitorpamplona.quartz.nip19Bech32.entities.Note -> { + is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> { LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt index 18d732481f..aad61b5cab 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip19Bech32.entities.Note import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -53,10 +53,10 @@ class NewMessageTaggerKeyParseTest { val result = NewMessageTagger(message = "", dao = dao) .parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn") - assertTrue(result?.key?.entity is Note) + assertTrue(result?.key?.entity is NNote) assertEquals( "1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", - (result?.key?.entity as? Note)?.hex, + (result?.key?.entity as? NNote)?.hex, ) assertEquals(null, result?.restOfWord) } @@ -79,10 +79,10 @@ class NewMessageTaggerKeyParseTest { val result = NewMessageTagger(message = "", dao = dao) .parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,") - assertTrue(result?.key?.entity is Note) + assertTrue(result?.key?.entity is NNote) assertEquals( "1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", - (result?.key?.entity as? Note)?.hex, + (result?.key?.entity as? NNote)?.hex, ) assertEquals(",", result?.restOfWord) } @@ -105,10 +105,10 @@ class NewMessageTaggerKeyParseTest { val result = NewMessageTagger(message = "", dao = dao) .parseDirtyWordForKey("@note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,") - assertTrue(result?.key?.entity is Note) + assertTrue(result?.key?.entity is NNote) assertEquals( "1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", - (result?.key?.entity as? Note)?.hex, + (result?.key?.entity as? NNote)?.hex, ) assertEquals(",", result?.restOfWord) } @@ -133,10 +133,10 @@ class NewMessageTaggerKeyParseTest { .parseDirtyWordForKey( "nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,", ) - assertTrue(result?.key?.entity is Note) + assertTrue(result?.key?.entity is NNote) assertEquals( "1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", - (result?.key?.entity as? Note)?.hex, + (result?.key?.entity as? NNote)?.hex, ) assertEquals(",", result?.restOfWord) } @@ -163,10 +163,10 @@ class NewMessageTaggerKeyParseTest { .parseDirtyWordForKey( "Nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,", ) - assertTrue(result?.key?.entity is Note) + assertTrue(result?.key?.entity is NNote) assertEquals( "1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", - (result?.key?.entity as? Note)?.hex, + (result?.key?.entity as? NNote)?.hex, ) assertEquals(",", result?.restOfWord) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt index 638de6571b..b3ca2fc53c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt @@ -28,8 +28,29 @@ import com.vitorpamplona.quartz.experimental.zapPolls.tags.MinimumTag import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @@ -41,7 +62,56 @@ class PollNoteEvent( tags: Array>, content: String, sig: HexKey, -) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints(): List { + val eHints = tags.mapNotNull(MarkedETag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() + + return eHints + qHints + nip19Hints + } + + override fun linkedEventIds(): List { + val eHints = tags.mapNotNull(MarkedETag::parseId) + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return eHints + qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(ATag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(ATag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } + + override fun pubKeyHints(): List { + val pHints = tags.mapNotNull(PTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() + + return pHints + nip19Hints + } + + override fun linkedPubKeys(): List { + val pHints = tags.mapNotNull(PTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } + fun pollOptionsArray() = tags.mapNotNull(PollOptionTag::parse) fun pollOptions() = pollOptionsArray().associate { it.index to it.descriptor } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProviders.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProviders.kt index e6ddd472d9..704ebd770a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProviders.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/hints/HintProviders.kt @@ -20,18 +20,25 @@ */ package com.vitorpamplona.quartz.nip01Core.hints +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint interface EventHintProvider { fun eventHints(): List + + fun linkedEventIds(): List } interface AddressHintProvider { fun addressHints(): List + + fun linkedAddressIds(): List } interface PubKeyHintProvider { fun pubKeyHints(): List + + fun linkedPubKeys(): List } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 80bb399f45..6ff4c37c57 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -415,7 +415,7 @@ open class BasicRelayClient( ) } socket?.let { - // Log.d(logTag, "Sending: $str") + Log.d(logTag, "Sending: $str") val result = it.send(str) listener.onSend(this@BasicRelayClient, str, result) stats.addBytesSent(str.bytesUsedInMemory()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt index 2f8cda9fd1..1b712af7ab 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.people -import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.Tag @@ -80,10 +79,7 @@ data class PTag( fun parseKey(tag: Array): HexKey? { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } - ensure(tag[1].length == 64) { - Log.w("PTag", "Invalid `$TAG_NAME` value ${tag.joinToString(", ")}") - return null - } + ensure(tag[1].length == 64) { return null } return tag[1] } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt index c91a6550b0..56f46bad2e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt @@ -31,10 +31,13 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash import com.vitorpamplona.quartz.nip01Core.tags.hashtags.countHashtags import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip02FollowList.tags.AddressFollowTag import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag @@ -54,8 +57,12 @@ class ContactListEvent( PubKeyHintProvider { override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + override fun pubKeyHints() = tags.mapNotNull(ContactTag::parseAsHint) + override fun linkedPubKeys() = tags.mapNotNull(ContactTag::parseKey) + /** * Returns a list of p-tags that are verified as hex keys. */ diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt index e9125efcc9..d05879a26a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsEvent.kt @@ -45,6 +45,8 @@ class OtsEvent( EventHintProvider { override fun eventHints() = tags.mapNotNull(TargetEventTag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(TargetEventTag::parseId) + override fun isContentEncoded() = true fun digestEventId() = tags.firstNotNullOfOrNull(TargetEventTag::parseId) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt index 592949a0a9..45df04bd00 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt @@ -55,8 +55,12 @@ class DeletionEvent( AddressHintProvider { override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + fun deleteEvents() = taggedEvents() fun deleteEventIds() = taggedEventIds() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index e44910158c..402bbb0bcd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -23,27 +23,23 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider -import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider -import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithEventsOrAddresses import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithPeople import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip19Bech32.entities.Entity import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip19Bech32.entities.Note import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull +import kotlin.collections.plus @Immutable open class BaseThreadedEvent( @@ -54,21 +50,15 @@ open class BaseThreadedEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, kind, tags, content, sig), - EventHintProvider, - AddressHintProvider, - PubKeyHintProvider { +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { @Transient private var citedUsersCache: Set? = null @Transient private var citedNotesCache: Set? = null - override fun eventHints() = tags.mapNotNull(MarkedETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) - - override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) - - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + @Transient + private var citedNIP19Cache: List? = null fun mentions() = taggedUsers() @@ -95,16 +85,6 @@ open class BaseThreadedEvent( ?: markedRoot()?.eventId ?: unmarkedReply()?.eventId - /* - Not sure if this is needed - open fun replyingToAddress(): ATag? { - val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && it[0] == "a" }?.let { ATag.parseAtag(it[1], it[2]) } - val newStyleReply = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "reply" }?.let { ATag.parseAtag(it[1], it[2]) } - val newStyleRoot = tags.lastOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" }?.let { ATag.parseAtag(it[1], it[2]) } - - return newStyleReply ?: newStyleRoot ?: oldStylePositional - }*/ - open fun replyingToAddressOrEvent(): String? { val oldStylePositional = tags.lastOrNull { it.size > 1 && it.size <= 3 && (it[0] == "e" || it[0] == "a") }?.get(1) val newStyleReply = tags.lastOrNull { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "reply" }?.get(1) @@ -113,6 +93,14 @@ open class BaseThreadedEvent( return newStyleReply ?: newStyleRoot ?: oldStylePositional } + fun citedNIP19(): List { + citedNIP19Cache?.let { + return it + } + + return findNostrUris(content).also { citedNIP19Cache = it } + } + fun citedUsers(): Set { citedUsersCache?.let { return it @@ -121,7 +109,7 @@ open class BaseThreadedEvent( val citedUsers = mutableSetOf() findIndexTagsWithPeople(content, tags, citedUsers) - findNostrUris(content).forEach { parsed -> + citedNIP19().forEach { parsed -> when (parsed) { is NProfile -> citedUsers.add(parsed.hex) is NPub -> citedUsers.add(parsed.hex) @@ -140,11 +128,11 @@ open class BaseThreadedEvent( val citations = mutableSetOf() findIndexTagsWithEventsOrAddresses(content, tags, citations).toMutableSet() - findNostrUris(content).forEach { entity -> + citedNIP19().forEach { entity -> when (entity) { is NEvent -> citations.add(entity.hex) is NAddress -> citations.add(entity.aTag()) - is Note -> citations.add(entity.hex) + is NNote -> citations.add(entity.hex) is NEmbed -> citations.add(entity.event.id) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt index 8ef8ce6bb4..061d7a26a1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -23,10 +23,31 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.tags.markedETags import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @@ -38,7 +59,56 @@ class TextNoteEvent( tags: Array>, content: String, sig: HexKey, -) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints(): List { + val eHints = tags.mapNotNull(MarkedETag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() + + return eHints + qHints + nip19Hints + } + + override fun linkedEventIds(): List { + val eHints = tags.mapNotNull(MarkedETag::parseId) + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return eHints + qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(ATag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(ATag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } + + override fun pubKeyHints(): List { + val pHints = tags.mapNotNull(PTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() + + return pHints + nip19Hints + } + + override fun linkedPubKeys(): List { + val pHints = tags.mapNotNull(PTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } + companion object { const val KIND = 1 const val ALT = "A short note: " diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt index 576b04b178..98870c3745 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt @@ -104,7 +104,7 @@ data class MarkedETag( @JvmStatic fun parseId(tag: Array): HexKey? { - ensure(tag.has(2)) { return null } + ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt index 81f7ad813c..807329a8b1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt @@ -42,6 +42,8 @@ open class BaseDMGroupEvent( PubKeyHintProvider { override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + /** Recipients intended to receive this conversation */ fun recipients() = tags.mapNotNull(PTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt index 2439e25cf1..a47bbd2753 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt @@ -56,10 +56,16 @@ class GenericRepostEvent( AddressHintProvider { override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + fun boostedEvents() = tags.mapNotNull(ETag::parse) fun boostedATags() = tags.mapNotNull(ATag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt index 05aa903e45..8dbb539c1a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.mapNotNull @Immutable class RepostEvent( @@ -54,10 +55,16 @@ class RepostEvent( AddressHintProvider { override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + fun boostedEvents() = tags.mapNotNull(ETag::parse) fun boostedATags() = tags.mapNotNull(ATag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt index f9e3d746a8..927eb0c872 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/EntityExt.kt @@ -26,10 +26,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip19Bech32.entities.Note -import org.apache.commons.lang3.text.translate.CharSequenceTranslator.hex +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote -fun Note.toQuoteTag() = QEventTag(hex, null, null) +fun NNote.toQuoteTag() = QEventTag(hex, null, null) fun NEvent.toQuoteTag() = QEventTag(hex, relay.firstOrNull(), author) @@ -42,7 +41,7 @@ fun NEmbed.toQuoteTag() = QEventTag(event.id, null, event.pubKey) } -fun Note.toQuoteTagArray() = QEventTag.assemble(hex, null, null) +fun NNote.toQuoteTagArray() = QEventTag.assemble(hex, null, null) fun NEvent.toQuoteTagArray() = QEventTag.assemble(hex, relay.firstOrNull(), author) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt index ae275a8dc8..14c74e3afe 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/QTag.kt @@ -63,12 +63,20 @@ interface QTag { } @JvmStatic - fun parseKey(tag: Array): String? { + fun parseId(tag: Array): String? { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } return tag[1] } + @JvmStatic + fun parseEventId(tag: Array): HexKey? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + return tag[1] + } + @JvmStatic fun parseEventAsHint(tag: Array): EventIdHint? { ensure(tag.has(2)) { return null } @@ -82,6 +90,14 @@ interface QTag { return EventIdHint(tag[1], relayHint) } + fun parseAddressId(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length != 64) { return null } + ensure(!tag[1].contains(':')) { return null } + return tag[1] + } + @JvmStatic fun parseAddressAsHint(tag: Array): AddressHint? { ensure(tag.has(2)) { return null } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt index 11a1f3c93c..74aea66ea6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayBuilderExt.kt @@ -27,9 +27,9 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.Entity import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip19Bech32.entities.Note fun TagArrayBuilder.quote(tag: QTag) = add(tag.toTagArray()) @@ -37,7 +37,7 @@ fun TagArrayBuilder.quotes(tag: List) = addAll(tag.map { it fun TagArrayBuilder.quote(entity: Entity) = when (entity) { - is Note -> add(entity.toQuoteTagArray()) + is NNote -> add(entity.toQuoteTagArray()) is NEvent -> add(entity.toQuoteTagArray()) is NAddress -> add(entity.toQuoteTagArray()) is NEmbed -> add(entity.toQuoteTagArray()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayExt.kt index ffd0bd5f73..84a46fcc79 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/quotes/TagArrayExt.kt @@ -32,7 +32,7 @@ fun TagArray.mapTaggedQuoteId(map: (eventId: HexKey) -> R) = this.mapValueTa fun TagArray.taggedQuotes() = this.mapNotNull(QTag::parse) -fun TagArray.taggedQuoteIds() = this.mapNotNull(QTag::parseKey) +fun TagArray.taggedQuoteIds() = this.mapNotNull(QTag::parseId) fun TagArray.firstTaggedQuote() = this.firstNotNullOfOrNull(QTag.Companion::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ListEntityExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ListEntityExt.kt new file mode 100644 index 0000000000..38ec044925 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ListEntityExt.kt @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip19Bech32 + +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip19Bech32.entities.Entity +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip19Bech32.entities.NSec +import org.apache.commons.lang3.text.translate.CharSequenceTranslator.hex +import kotlin.collections.mapNotNull + +fun NEvent.toEventHint() = relay.map { EventIdHint(hex, it) } + +fun NAddress.toAddressHint() = relay.map { AddressHint(aTag(), it) } + +fun NProfile.toPubKeyHint() = relay.map { PubKeyHint(hex, it) } + +fun List.eventHints(): List = + mapNotNull { entity -> + if (entity is NEvent) { + entity.toEventHint() + } else { + null + } + }.flatten() + +fun List.eventIds(): List = + mapNotNull { entity -> + when (entity) { + is NEvent -> entity.hex + is NNote -> entity.hex + is NEmbed -> entity.event.id + else -> null + } + } + +fun List.addressHints(): List = + mapNotNull { entity -> + if (entity is NAddress) { + entity.toAddressHint() + } else { + null + } + }.flatten() + +fun List.addressIds(): List = + mapNotNull { entity -> + when (entity) { + is NAddress -> entity.aTag() + is NEmbed -> if (entity.event is AddressableEvent) entity.event.addressTag() else null + else -> null + } + } + +fun List.pubKeyHints(): List = + mapNotNull { entity -> + if (entity is NProfile) { + entity.relay.map { PubKeyHint(entity.hex, it) } + } else { + null + } + }.flatten() + +fun List.pubKeys(): List = + mapNotNull { entity -> + when (entity) { + is NProfile -> entity.hex + is NPub -> entity.hex + is NSec -> entity.hex + else -> null + } + } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt index 140706a7cf..4afd159f6c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt @@ -31,11 +31,11 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.Entity import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec -import com.vitorpamplona.quartz.nip19Bech32.entities.Note import com.vitorpamplona.quartz.utils.Hex import kotlinx.coroutines.CancellationException import java.util.regex.Pattern @@ -121,7 +121,7 @@ object Nip19Parser { when (type.lowercase()) { "nsec1" -> NSec.parse(bytes) "npub1" -> NPub.parse(bytes) - "note1" -> Note.parse(bytes) + "note1" -> NNote.parse(bytes) "nprofile1" -> NProfile.parse(bytes) "nevent1" -> NEvent.parse(bytes) "nrelay1" -> NRelay.parse(bytes) @@ -177,7 +177,7 @@ fun decodePrivateKeyAsHexOrNull(key: String): HexKey? = is NSec -> parsed.hex is NPub -> null is NProfile -> null - is Note -> null + is NNote -> null is NEvent -> null is NEmbed -> null is NRelay -> null @@ -195,7 +195,7 @@ fun decodePublicKeyAsHexOrNull(key: String): HexKey? = is NSec -> Nip01.pubKeyCreate(parsed.hex.hexToByteArray()).toHexKey() is NPub -> parsed.hex is NProfile -> parsed.hex - is Note -> null + is NNote -> null is NEvent -> null is NEmbed -> null is NRelay -> null @@ -213,7 +213,7 @@ fun decodeEventIdAsHexOrNull(key: String): HexKey? = is NSec -> null is NPub -> null is NProfile -> null - is Note -> parsed.hex + is NNote -> parsed.hex is NEvent -> parsed.hex is NAddress -> parsed.aTag() is NEmbed -> null diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/Note.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NNote.kt similarity index 92% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/Note.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NNote.kt index 6ac4c7c143..89bb869e8b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/Note.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NNote.kt @@ -27,13 +27,13 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip19Bech32.toNote @Immutable -data class Note( +data class NNote( val hex: String, ) : Entity { - companion object { - fun parse(bytes: ByteArray): Note? { + companion object Companion { + fun parse(bytes: ByteArray): NNote? { if (bytes.isEmpty()) return null - return Note(bytes.toHexKey()) + return NNote(bytes.toHexKey()) } fun create(eventId: HexKey): String = eventId.hexToByteArray().toNote() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt index 0aaf57f239..7713994ea9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt @@ -29,9 +29,24 @@ import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri import com.vitorpamplona.quartz.nip22Comments.tags.ReplyAddressTag import com.vitorpamplona.quartz.nip22Comments.tags.ReplyAuthorTag @@ -62,11 +77,55 @@ class CommentEvent( EventHintProvider, PubKeyHintProvider, AddressHintProvider { - override fun pubKeyHints() = tags.mapNotNull(RootAuthorTag::parseAsHint) + tags.mapNotNull(ReplyAuthorTag::parseAsHint) + override fun pubKeyHints(): List { + val pHints = + tags.mapNotNull(RootAuthorTag::parseAsHint) + + tags.mapNotNull(ReplyAuthorTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() - override fun eventHints() = tags.mapNotNull(RootEventTag::parseAsHint) + tags.mapNotNull(ReplyEventTag::parseAsHint) + return pHints + nip19Hints + } - override fun addressHints() = tags.mapNotNull(RootAddressTag::parseAsHint) + tags.mapNotNull(ReplyAddressTag::parseAsHint) + override fun linkedPubKeys(): List { + val pHints = + tags.mapNotNull(RootAuthorTag::parseKey) + + tags.mapNotNull(ReplyAuthorTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } + + override fun eventHints(): List { + val eHints = tags.mapNotNull(RootEventTag::parseAsHint) + tags.mapNotNull(ReplyEventTag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() + + return eHints + qHints + nip19Hints + } + + override fun linkedEventIds(): List { + val eHints = tags.mapNotNull(RootEventTag::parseKey) + tags.mapNotNull(ReplyEventTag::parseKey) + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return eHints + qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(RootAddressTag::parseAsHint) + tags.mapNotNull(ReplyAddressTag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(RootAddressTag::parseAddressId) + tags.mapNotNull(ReplyAddressTag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } fun rootAuthor() = tags.firstNotNullOfOrNull(RootAuthorTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt index e130211f10..4a026c577b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt @@ -27,6 +27,9 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag @@ -35,7 +38,14 @@ import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag @@ -59,11 +69,47 @@ class LongTextNoteEvent( PubKeyHintProvider, AddressHintProvider, RootScope { - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun eventHints(): List { + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() - override fun eventHints() = tags.mapNotNull(QTag::parseEventAsHint) + return qHints + nip19Hints + } - override fun addressHints() = tags.mapNotNull(QTag::parseAddressAsHint) + override fun linkedEventIds(): List { + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return qHints + nip19Hints + } + + override fun addressHints(): List { + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return qHints + nip19Hints + } + + override fun pubKeyHints(): List { + val pHints = tags.mapNotNull(PTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() + + return pHints + nip19Hints + } + + override fun linkedPubKeys(): List { + val pHints = tags.mapNotNull(PTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } override fun dTag() = tags.dTag() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt index 5fc3948b10..3c11b0d017 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip25Reactions/ReactionEvent.kt @@ -54,10 +54,16 @@ class ReactionEvent( AddressHintProvider { override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + fun originalPost() = tags.mapNotNull(ETag::parseId) fun originalAuthor() = tags.mapNotNull(PTag::parseKey) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index c7164a668b..5953c078a2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -48,6 +48,8 @@ class ChannelCreateEvent( EventHintProvider { override fun eventHints() = channelInfo().relays?.map { EventIdHint(id, it) } ?: emptyList() + override fun linkedEventIds() = listOf(id) + fun channelInfo(): ChannelDataNorm = try { ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt index eaa611d323..66b579577d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelHideMessageEvent.kt @@ -46,6 +46,8 @@ class ChannelHideMessageEvent( EventHintProvider { override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + fun eventsToHide() = tags.taggedEventIds() companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt index d46c0ece8e..6446edcef2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt @@ -48,7 +48,12 @@ class ChannelMetadataEvent( sig: HexKey, ) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider { - override fun eventHints() = channelInfo().relays?.map { EventIdHint(id, it) } ?: emptyList() + override fun eventHints() = + channelInfo().relays?.mapNotNull { relay -> + channelId()?.let { EventIdHint(it, relay) } + } ?: emptyList() + + override fun linkedEventIds() = channelId()?.let { listOf(it) } ?: emptyList() fun channelInfo(): ChannelDataNorm = try { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt index 634e9e8473..3d3b964034 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMuteUserEvent.kt @@ -46,6 +46,8 @@ class ChannelMuteUserEvent( PubKeyHintProvider { override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + fun usersToMute() = tags.taggedUserIds() companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt index 37ff66f2b4..32fd1e3ced 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt @@ -24,11 +24,32 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.tagArray +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.tags.markedETag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel import com.vitorpamplona.quartz.nip28PublicChat.base.channel @@ -46,7 +67,56 @@ class ChannelMessageEvent( sig: HexKey, ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), IsInPublicChatChannel, - ExposeInDraft { + ExposeInDraft, + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints(): List { + val eHints = tags.mapNotNull(MarkedETag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() + + return eHints + qHints + nip19Hints + } + + override fun linkedEventIds(): List { + val eHints = tags.mapNotNull(MarkedETag::parseId) + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return eHints + qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(ATag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(ATag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } + + override fun pubKeyHints(): List { + val pHints = tags.mapNotNull(PTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() + + return pHints + nip19Hints + } + + override fun linkedPubKeys(): List { + val pHints = tags.mapNotNull(PTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } + override fun channel() = markedRoot() ?: unmarkedRoot() override fun channelId() = channel()?.eventId diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt index a4c62ff182..e4073aa414 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/selection/EmojiPackSelectionEvent.kt @@ -46,6 +46,8 @@ class EmojiPackSelectionEvent( AddressHintProvider { override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + fun emojiPacks() = tags.mapNotNull(ATag::parseAddress) fun emojiPackIds() = tags.mapNotNull(ATag::parseAddressId) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt index 806685606b..e5f90580b9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt @@ -27,13 +27,23 @@ import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip14Subject.SubjectTag import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -50,11 +60,49 @@ class GitIssueEvent( PubKeyHintProvider, EventHintProvider, AddressHintProvider { - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun eventHints(): List { + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() - override fun eventHints() = tags.mapNotNull(QTag::parseEventAsHint) + return qHints + nip19Hints + } - override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + override fun linkedEventIds(): List { + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(ATag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(ATag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } + + override fun pubKeyHints(): List { + val pHints = tags.mapNotNull(PTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() + + return pHints + nip19Hints + } + + override fun linkedPubKeys(): List { + val pHints = tags.mapNotNull(PTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } fun repositoryHex() = tags.firstNotNullOfOrNull(ATag::parseAddressId) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt index 243ec363bd..dd87ff02bd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt @@ -29,6 +29,8 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseId import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip31Alts.AltTag @@ -48,10 +50,16 @@ class GitPatchEvent( AddressHintProvider { override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(MarkedETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(MarkedETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + private fun innerRepository() = tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "root" } ?: tags.firstOrNull { it.size > 1 && it[0] == "a" } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt index 9e7ccb4092..894d6701a3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt @@ -27,14 +27,25 @@ import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip10Notes.tags.markedETags import com.vitorpamplona.quartz.nip10Notes.tags.prepareMarkedETagsAsReplyTo import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -53,11 +64,51 @@ class GitReplyEvent( PubKeyHintProvider, EventHintProvider, AddressHintProvider { - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun eventHints(): List { + val eHints = tags.mapNotNull(ETag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() - override fun eventHints() = tags.mapNotNull(MarkedETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + return eHints + qHints + nip19Hints + } - override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + override fun linkedEventIds(): List { + val eHints = tags.mapNotNull(ETag::parseId) + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return eHints + qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(ATag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(ATag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } + + override fun pubKeyHints(): List { + val pHints = tags.mapNotNull(PTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() + + return pHints + nip19Hints + } + + override fun linkedPubKeys(): List { + val pHints = tags.mapNotNull(PTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } fun repositoryHex() = tags.firstNotNullOfOrNull(ATag::parseAddressId) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt index 99b8fc2513..9ef7531d39 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt @@ -27,8 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTags import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents @@ -37,6 +41,12 @@ import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip10Notes.tags.positionalMarkedTags import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils @@ -53,11 +63,49 @@ class TorrentCommentEvent( EventHintProvider, PubKeyHintProvider, AddressHintProvider { - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun eventHints(): List { + val eHints = tags.mapNotNull(MarkedETag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() - override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + return eHints + qHints + nip19Hints + } - override fun addressHints() = tags.mapNotNull(QTag::parseAddressAsHint) + override fun linkedEventIds(): List { + val eHints = tags.mapNotNull(MarkedETag::parseId) + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return eHints + qHints + nip19Hints + } + + override fun addressHints(): List { + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return qHints + nip19Hints + } + + override fun pubKeyHints(): List { + val pHints = tags.mapNotNull(PTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() + + return pHints + nip19Hints + } + + override fun linkedPubKeys(): List { + val pHints = tags.mapNotNull(PTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } fun torrent() = tags.firstNotNullOfOrNull(MarkedETag::parseRoot) ?: tags.firstNotNullOfOrNull(ETag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt index 5e2d0dade2..e785d00fa8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt @@ -58,11 +58,17 @@ class DraftEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) @Transient private var cachedInnerEvent: Map = mapOf() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt index e50c41beb3..20e7738402 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt @@ -56,10 +56,16 @@ abstract class GeneralListEvent( PubKeyHintProvider { override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(ETag::parseAsHint) ?: emptyList()) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + (cachedPrivateTags()?.mapNotNull(ETag::parseId) ?: emptyList()) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(ATag::parseAsHint) ?: emptyList()) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + (cachedPrivateTags()?.mapNotNull(ATag::parseAddressId) ?: emptyList()) + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(PTag::parseAsHint) ?: emptyList()) + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + (cachedPrivateTags()?.mapNotNull(PTag::parseKey) ?: emptyList()) + fun name() = tags.firstNotNullOfOrNull(NameTag::parse) @Deprecated("NIP-51 has deprecated Title. Use name instead", ReplaceWith("name()")) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt index e15892394c..11ef4ec951 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt @@ -28,6 +28,9 @@ import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.aTag @@ -36,6 +39,12 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.tags.markedETag import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip37Drafts.ExposeInDraft import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -53,11 +62,51 @@ class LiveActivitiesChatMessageEvent( PubKeyHintProvider, AddressHintProvider, ExposeInDraft { - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun eventHints(): List { + val eHints = tags.mapNotNull(ETag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() - override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + return eHints + qHints + nip19Hints + } - override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + override fun linkedEventIds(): List { + val eHints = tags.mapNotNull(ETag::parseId) + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return eHints + qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(ATag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(ATag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } + + override fun pubKeyHints(): List { + val pHints = tags.mapNotNull(PTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() + + return pHints + nip19Hints + } + + override fun linkedPubKeys(): List { + val pHints = tags.mapNotNull(PTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } private fun activityHex() = tags.firstNotNullOfOrNull(ATag::parseAddressId) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt index df9a1dc4c2..293865dc34 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parse import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseKey import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint @@ -65,10 +66,16 @@ class LiveActivitiesEvent( PubKeyHintProvider { override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + tags.mapNotNull(QTag::parseEventId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + tags.mapNotNull(QTag::parseAddressId) + override fun pubKeyHints() = tags.mapNotNull(ParticipantTag::parseAsHint) + override fun linkedPubKeys() = tags.mapNotNull(ParticipantTag::parseKey) + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index 823edee0d6..cc490826a2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -23,13 +23,28 @@ package com.vitorpamplona.quartz.nip54Wiki import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -42,7 +57,56 @@ class WikiNoteEvent( content: String, sig: HexKey, ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), - AddressableEvent { + AddressableEvent, + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun eventHints(): List { + val eHints = tags.mapNotNull(MarkedETag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() + + return eHints + qHints + nip19Hints + } + + override fun linkedEventIds(): List { + val eHints = tags.mapNotNull(MarkedETag::parseId) + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return eHints + qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(ATag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(ATag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } + + override fun pubKeyHints(): List { + val pHints = tags.mapNotNull(PTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() + + return pHints + nip19Hints + } + + override fun linkedPubKeys(): List { + val pHints = tags.mapNotNull(PTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } + override fun dTag() = tags.dTag() override fun aTag(relayHint: NormalizedRelayUrl?) = ATag(kind, pubKey, dTag(), relayHint) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt index 28c669c91e..245d0aa91f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt @@ -51,11 +51,17 @@ class LnZapEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) // This event is also kept in LocalCache (same object) @Transient val zapRequest: LnZapRequestEvent? diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt index 071f95c149..edbe70ff15 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt @@ -48,11 +48,17 @@ class LnZapPrivateEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) companion object { const val KIND = 9733 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt index 8df6711324..04a7d450d2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt @@ -52,11 +52,17 @@ class LnZapRequestEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) @Transient private var privateZapEvent: LnZapPrivateEvent? = null diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt index be7996a23a..519b13206a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt @@ -48,11 +48,17 @@ class BadgeAwardEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) fun awardees() = taggedUsers() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt index 9488881d92..84ab8aa2bf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt @@ -47,11 +47,17 @@ class BadgeProfilesEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) fun badgeAwardEvents() = taggedEvents() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt index 726a0b8193..8cb0fb897a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt @@ -44,6 +44,8 @@ import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag.Companion.parseKey import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -61,10 +63,16 @@ class CommunityPostApprovalEvent( PubKeyHintProvider { override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + tags.mapNotNull(QTag::parseEventId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + tags.mapNotNull(QTag::parseAddressId) + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + fun containedPost(): Event? = try { content.ifBlank { null }?.let { fromJson(it) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt index 51feebe0d9..c18c9b1a2b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt @@ -39,6 +39,8 @@ import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag.Companion.parseAsHint +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag.Companion.parseKey import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.DescriptionTag import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ImageTag import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag @@ -62,13 +64,19 @@ class CommunityDefinitionEvent( PubKeyHintProvider { override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + tags.mapNotNull(QTag::parseEventId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + - tags.mapNotNull(RelayTag::parse).mapNotNull { AddressHint(addressTag(), it.url) } + tags.mapNotNull(RelayTag::parse).map { AddressHint(addressTag(), it.url) } + + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + tags.mapNotNull(QTag::parseAddressId) override fun pubKeyHints() = tags.mapNotNull(ModeratorTag::parseAsHint) + override fun linkedPubKeys() = tags.mapNotNull(ModeratorTag::parseKey) + fun name() = tags.firstNotNullOfOrNull(NameTag::parse) fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt index ba33b8abb0..2bccc47901 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/tags/ModeratorTag.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.ensure @@ -65,7 +64,7 @@ data class ModeratorTag( @JvmStatic fun parseAsHint(tag: Array): PubKeyHint? { ensure(tag.has(2)) { return null } - ensure(tag[0] == ParticipantTag.Companion.TAG_NAME) { return null } + ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].length == 64) { return null } ensure(tag[2].isNotEmpty()) { return null } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt index f55600e480..7ee4f54400 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt @@ -59,11 +59,17 @@ class GoalEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) fun topics() = hashtags() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt index 7fbad7d951..d1c17157ee 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt @@ -25,6 +25,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint @@ -41,6 +44,12 @@ import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip84Highlights.tags.CommentTag @@ -60,11 +69,51 @@ class HighlightEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider { - override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + tags.mapNotNull(QTag::parseEventAsHint) + override fun eventHints(): List { + val eHints = tags.mapNotNull(ETag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() - override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + tags.mapNotNull(QTag::parseAddressAsHint) + return eHints + qHints + nip19Hints + } - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + override fun linkedEventIds(): List { + val eHints = tags.mapNotNull(ETag::parseId) + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return eHints + qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(ATag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(ATag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } + + override fun pubKeyHints(): List { + val pHints = tags.mapNotNull(PTag::parseAsHint) + val nip19Hints = citedNIP19().pubKeyHints() + + return pHints + nip19Hints + } + + override fun linkedPubKeys(): List { + val pHints = tags.mapNotNull(PTag::parseKey) + val nip19Hints = citedNIP19().pubKeys() + + return pHints + nip19Hints + } fun inUrl() = tags.firstNotNullOfOrNull(ReferenceTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt index c8266e82a5..04291e4dfa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/recommendation/AppRecommendationEvent.kt @@ -46,6 +46,8 @@ class AppRecommendationEvent( AddressHintProvider { override fun addressHints() = tags.mapNotNull(RecommendationTag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(RecommendationTag::parseAddressId) + fun recommendations() = tags.mapNotNull(RecommendationTag::parse) fun recommendationAddresses() = tags.mapNotNull(RecommendationTag::parseAddressId) diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt index f1143e1ce3..c4609ba934 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip19Bech32/NIP19ParserTest.kt @@ -26,9 +26,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip19Bech32.entities.Note import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -65,7 +65,7 @@ class NIP19ParserTest { @Test() fun uri_to_route_note() { val result = - Nip19Parser.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv")?.entity as? Note + Nip19Parser.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv")?.entity as? NNote assertNotNull(result) Assert.assertEquals( From d084714a2175b9fec10ff4c1d728c9bb2dab621d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 7 Jul 2025 17:19:37 -0400 Subject: [PATCH 039/185] Avoids logging all REQs by default. --- .../nip01Core/relay/client/single/basic/BasicRelayClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 6ff4c37c57..80bb399f45 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -415,7 +415,7 @@ open class BasicRelayClient( ) } socket?.let { - Log.d(logTag, "Sending: $str") + // Log.d(logTag, "Sending: $str") val result = it.send(str) listener.onSend(this@BasicRelayClient, str, result) stats.addBytesSent(str.bytesUsedInMemory()) From b800b5a664014fba6a0c58e5e1d7939156507c4f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 7 Jul 2025 19:49:23 -0400 Subject: [PATCH 040/185] Early support for Blocked and Trusted Relay Lists --- .../amethyst/LocalPreferences.kt | 26 ++++ .../vitorpamplona/amethyst/model/Account.kt | 24 +++- .../amethyst/model/AccountSettings.kt | 24 ++++ .../amethyst/model/LocalCache.kt | 16 +++ .../FollowListOutboxRelays.kt | 37 +++--- .../nip02FollowLists/FollowsPerOutboxRelay.kt | 40 ++++--- .../model/nip51Lists/BlockedRelayListState.kt | 113 ++++++++++++++++++ .../model/nip51Lists/TrustedRelayListState.kt | 113 ++++++++++++++++++ .../serverList/TrustedRelayListsState.kt | 49 ++++---- .../topNavFeeds/FeedTopNavFilterState.kt | 5 +- .../AllFollowsByOutboxTopNavFilter.kt | 7 +- .../allFollows/AllFollowsFeedFlow.kt | 3 + .../topNavFeeds/noteBased/NoteFeedFlow.kt | 41 ++++--- .../AllCommunitiesTopNavFilter.kt | 22 ++-- .../author/AuthorsByOutboxTopNavFilter.kt | 17 ++- .../community/SingleCommunityTopNavFilter.kt | 45 +++---- .../muted/MutedAuthorsByOutboxTopNavFilter.kt | 17 ++- .../FilterAccountInfoAndListsFromKey.kt | 18 +++ .../FilterBasicAccountInfoFromKeys.kt | 4 + .../amethyst/ui/navigation/AppNavigation.kt | 2 +- .../amethyst/ui/navigation/DrawerContent.kt | 2 +- .../amethyst/ui/navigation/Routes.kt | 4 +- .../amethyst/ui/note/NoteCompose.kt | 6 + .../amethyst/ui/note/types/RelayList.kt | 62 +++++++++- .../FollowingEphemeralChatSubAssembler.kt | 2 +- .../FollowingPublicChatSubAssembler.kt | 2 +- .../DiscoveryFollowsDiscoverySubAssembler1.kt | 2 +- .../DiscoveryFollowsDiscoverySubAssembler2.kt | 2 +- .../DiscoveryFollowsDiscoverySubAssembler3.kt | 2 +- .../loggedIn/profile/relays/RelayFeedView.kt | 8 +- .../loggedIn/relays/AllRelayListScreen.kt | 38 +++++- .../relays/blocked/BlockedRelayListView.kt | 81 +++++++++++++ .../blocked/BlockedRelayListViewModel.kt | 32 +++++ .../relays/trusted/TrustedRelayListView.kt | 81 +++++++++++++ .../trusted/TrustedRelayListViewModel.kt | 32 +++++ .../loggedIn/threadview/ThreadFeedView.kt | 8 ++ .../VideoOutboxEventsFilterSubAssembler.kt | 2 +- amethyst/src/main/res/values/strings.xml | 9 ++ .../com/vitorpamplona/quartz/EventFactory.kt | 4 + .../nip01Core/relay/client/NostrClient.kt | 8 +- .../nip51Lists/BlockedRelayListEvent.kt | 105 ++++++++++++++++ .../nip51Lists/TrustedRelayListEvent.kt | 105 ++++++++++++++++ 42 files changed, 1088 insertions(+), 132 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BlockedRelayListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TrustedRelayListEvent.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index b9c909df51..bd52ded9a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -51,7 +51,9 @@ import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -96,6 +98,8 @@ private object PrefKeys { const val LATEST_DM_RELAY_LIST = "latestDMRelayList" const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList" const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList" + const val LATEST_BLOCKED_RELAY_LIST = "latestSearchRelayList" + const val LATEST_TRUSTED_RELAY_LIST = "latestSearchRelayList" const val LATEST_MUTE_LIST = "latestMuteList" const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList" const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData" @@ -367,6 +371,24 @@ object LocalPreferences { remove(PrefKeys.LATEST_SEARCH_RELAY_LIST) } + if (settings.backupBlockedRelayList != null) { + putString( + PrefKeys.LATEST_BLOCKED_RELAY_LIST, + EventMapper.mapper.writeValueAsString(settings.backupBlockedRelayList), + ) + } else { + remove(PrefKeys.LATEST_BLOCKED_RELAY_LIST) + } + + if (settings.backupTrustedRelayList != null) { + putString( + PrefKeys.LATEST_TRUSTED_RELAY_LIST, + EventMapper.mapper.writeValueAsString(settings.backupTrustedRelayList), + ) + } else { + remove(PrefKeys.LATEST_TRUSTED_RELAY_LIST) + } + if (settings.localRelayServers.value.isNotEmpty()) { putStringSet(PrefKeys.LOCAL_RELAY_SERVERS, settings.localRelayServers.value) } else { @@ -562,6 +584,8 @@ object LocalPreferences { val latestDmRelayList = parseEventOrNull(PrefKeys.LATEST_DM_RELAY_LIST) val latestNip65RelayList = parseEventOrNull(PrefKeys.LATEST_NIP65_RELAY_LIST) val latestSearchRelayList = parseEventOrNull(PrefKeys.LATEST_SEARCH_RELAY_LIST) + val latestBlockedRelayList = parseEventOrNull(PrefKeys.LATEST_BLOCKED_RELAY_LIST) + val latestTrustedRelayList = parseEventOrNull(PrefKeys.LATEST_TRUSTED_RELAY_LIST) val latestMuteList = parseEventOrNull(PrefKeys.LATEST_MUTE_LIST) val latestPrivateHomeRelayList = parseEventOrNull(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST) val latestAppSpecificData = parseEventOrNull(PrefKeys.LATEST_APP_SPECIFIC_DATA) @@ -604,6 +628,8 @@ object LocalPreferences { backupNIP65RelayList = latestNip65RelayList, backupDMRelayList = latestDmRelayList, backupSearchRelayList = latestSearchRelayList, + backupBlockedRelayList = latestBlockedRelayList, + backupTrustedRelayList = latestTrustedRelayList, backupPrivateHomeRelayList = latestPrivateHomeRelayList, backupMuteList = latestMuteList, backupAppSpecificData = latestAppSpecificData, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index f52cf5521c..84b08bb377 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -43,10 +43,12 @@ import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.BlockPeopleListState +import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.HashtagListState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.nip51Lists.MuteListState +import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState @@ -239,6 +241,8 @@ class Account( val searchRelayList = SearchRelayListState(signer, cache, scope, settings) val privateStorageRelayList = PrivateStorageRelayListState(signer, cache, scope, settings) val localRelayList = LocalRelayListState(signer, cache, scope, settings) + val trustedRelayList = TrustedRelayListState(signer, cache, scope, settings) + val blockedRelayList = BlockedRelayListState(signer, cache, scope, settings) val kind3FollowList = FollowListState(signer, cache, scope, settings) val ephemeralChatList = EphemeralChatListState(signer, cache, scope, settings) @@ -264,14 +268,14 @@ class Account( val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope) val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope) - val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, scope) + val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, trustedRelayList, scope) // Follows Relays - val followOutboxes = FollowListOutboxRelays(kind3FollowList, cache, scope) + val followOutboxes = FollowListOutboxRelays(kind3FollowList, blockedRelayList, cache, scope) val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, scope) // keeps a cache of the outbox relays for each author - val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, cache, scope).flow + val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow // Merges all follow lists to create a single All Follows feed. val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope) @@ -283,6 +287,7 @@ class Account( allFollows = allFollows.flow, locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, + blockedRelays = blockedRelayList.flow, signer = signer, scope = scope, ).flow @@ -295,6 +300,7 @@ class Account( allFollows = allFollows.flow, locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, + blockedRelays = blockedRelayList.flow, signer = signer, scope = scope, ).flow @@ -307,6 +313,7 @@ class Account( allFollows = allFollows.flow, locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, + blockedRelays = blockedRelayList.flow, signer = signer, scope = scope, ).flow @@ -319,6 +326,7 @@ class Account( allFollows = allFollows.flow, locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, + blockedRelays = blockedRelayList.flow, signer = signer, scope = scope, ).flow @@ -2100,6 +2108,16 @@ class Account( searchRelayList.saveRelayList(searchRelays, ::sendMyPublicAndPrivateOutbox) } + fun saveTrustedRelayList(trustedRelays: List) { + if (!isWriteable()) return + trustedRelayList.saveRelayList(trustedRelays, ::sendMyPublicAndPrivateOutbox) + } + + fun saveBlockedRelayList(blockedRelays: List) { + if (!isWriteable()) return + blockedRelayList.saveRelayList(blockedRelays, ::sendMyPublicAndPrivateOutbox) + } + fun sendNip65RelayList(relays: List) { if (!isWriteable()) return nip65RelayList.saveRelayList(relays, ::sendMyPublicAndPrivateOutbox) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 4f19f92c06..bfba3d93fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -40,7 +40,9 @@ import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncher @@ -116,6 +118,8 @@ class AccountSettings( var backupDMRelayList: ChatMessageRelayListEvent? = null, var backupNIP65RelayList: AdvertisedRelayListEvent? = null, var backupSearchRelayList: SearchRelayListEvent? = null, + var backupBlockedRelayList: BlockedRelayListEvent? = null, + var backupTrustedRelayList: TrustedRelayListEvent? = null, var backupMuteList: MuteListEvent? = null, var backupPrivateHomeRelayList: PrivateOutboxRelayListEvent? = null, var backupAppSpecificData: AppSpecificDataEvent? = null, @@ -351,6 +355,26 @@ class AccountSettings( } } + fun updateBlockedRelayList(newBlockedRelayList: BlockedRelayListEvent?) { + if (newBlockedRelayList == null || newBlockedRelayList.tags.isEmpty()) return + + // Events might be different objects, we have to compare their ids. + if (backupBlockedRelayList?.id != newBlockedRelayList.id) { + backupBlockedRelayList = newBlockedRelayList + saveAccountSettings() + } + } + + fun updateTrustedRelayList(newTrustedRelayList: TrustedRelayListEvent?) { + if (newTrustedRelayList == null || newTrustedRelayList.tags.isEmpty()) return + + // Events might be different objects, we have to compare their ids. + if (backupTrustedRelayList?.id != newTrustedRelayList.id) { + backupTrustedRelayList = newTrustedRelayList + saveAccountSettings() + } + } + fun updatePrivateHomeRelayList(newPrivateHomeRelayList: PrivateOutboxRelayListEvent?) { if (newPrivateHomeRelayList == null || newPrivateHomeRelayList.tags.isEmpty()) return diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index c90eac1383..fccb6c74b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -125,12 +125,14 @@ import com.vitorpamplona.quartz.nip40Expiration.isExpired import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent +import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -972,6 +974,18 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( + event: BlockedRelayListEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + + private fun consume( + event: TrustedRelayListEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( event: CommunityDefinitionEvent, relay: NormalizedRelayUrl?, @@ -3045,6 +3059,7 @@ object LocalCache : ILocalCache { is BadgeAwardEvent -> consume(event, relay, wasVerified) is BadgeDefinitionEvent -> consume(event, relay, wasVerified) is BadgeProfilesEvent -> consume(event, relay, wasVerified) + is BlockedRelayListEvent -> consume(event, relay, wasVerified) is BlossomServersEvent -> consume(event, relay, wasVerified) is BookmarkListEvent -> consume(event, relay, wasVerified) is CalendarEvent -> consume(event, relay, wasVerified) @@ -3126,6 +3141,7 @@ object LocalCache : ILocalCache { is TextNoteModificationEvent -> consume(event, relay, wasVerified) is TorrentEvent -> consume(event, relay, wasVerified) is TorrentCommentEvent -> consume(event, relay, wasVerified) + is TrustedRelayListEvent -> consume(event, relay, wasVerified) is VideoHorizontalEvent -> consume(event, relay, wasVerified) is VideoVerticalEvent -> consume(event, relay, wasVerified) is WikiNoteEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt index 32bab41605..7bfeaa71b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -33,17 +34,20 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest +import kotlin.collections.flatten import kotlin.collections.map +import kotlin.collections.minus import kotlin.collections.toSet class FollowListOutboxRelays( kind3Follows: FollowListState, + blockedRelayList: BlockedRelayListState, val cache: LocalCache, scope: CoroutineScope, ) { @@ -57,29 +61,32 @@ class FollowListOutboxRelays( fun allRelayListFlows(followList: Set): List> = followList.map { getNIP65RelayListFlow(it) } - fun combineAllFlows(flows: List>): Flow> = + fun combineAllFlows( + flows: List>, + blockedRelays: Set, + ): Flow> = combine(flows) { relayListNotes: Array -> relayListNotes.mapNotNull { (it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() } }.map { - it.flatten().toSet() + it.flatten().toSet().minus(blockedRelays) } @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow> = - kind3Follows.flow - .transformLatest { followList -> - val flows: List> = allRelayListFlows(followList.authors) - val relayListFlows = combineAllFlows(flows) - emitAll(relayListFlows) - }.onStart { - kind3Follows.flow.value.authors - .mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm() - }.flatten() - .toSet() - }.flowOn(Dispatchers.Default) + combineTransform(kind3Follows.flow, blockedRelayList.flow) { followList, blockedRelays -> + val flows: List> = allRelayListFlows(followList.authors) + val relayListFlows = combineAllFlows(flows, blockedRelays) + emitAll(relayListFlows) + }.onStart { + val blocked = blockedRelayList.flow.value.toSet() + kind3Follows.flow.value.authors + .mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm()?.minus(blocked) + }.flatten() + .toSet() + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt index dfd57f9964..204ae9a6fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -34,15 +35,16 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest import kotlin.collections.map class FollowsPerOutboxRelay( kind3Follows: FollowListState, + blockedRelayList: BlockedRelayListState, val cache: LocalCache, scope: CoroutineScope, ) { @@ -56,13 +58,18 @@ class FollowsPerOutboxRelay( fun allRelayListFlows(followList: Set): List> = followList.map { getNIP65RelayListFlow(it) } - fun combineAllFlows(flows: List>): Flow>> = + fun combineAllFlows( + flows: List>, + blockedList: Set, + ): Flow>> = combine(flows) { relayListNotes: Array -> mapOfSet { relayListNotes.forEach { noteState -> noteState.note.author?.pubkeyHex?.let { authorHex -> (noteState.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.forEach { relay -> - add(relay, authorHex) + if (relay !in blockedList) { + add(relay, authorHex) + } } } } @@ -71,22 +78,23 @@ class FollowsPerOutboxRelay( @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow>> = - kind3Follows.flow - .transformLatest { followList -> - val flows: List> = allRelayListFlows(followList.authors) - val relayListFlows = combineAllFlows(flows) - emitAll(relayListFlows) - }.onStart { - emit( - mapOfSet { - kind3Follows.flow.value.authors.map { authorHex -> - getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay -> + combineTransform(kind3Follows.flow, blockedRelayList.flow) { followList, blockedList -> + val flows: List> = allRelayListFlows(followList.authors) + val relayListFlows = combineAllFlows(flows, blockedList) + emitAll(relayListFlows) + }.onStart { + emit( + mapOfSet { + kind3Follows.flow.value.authors.map { authorHex -> + getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay -> + if (relay !in blockedRelayList.flow.value) { add(relay, authorHex) } } - }, - ) - }.flowOn(Dispatchers.Default) + } + }, + ) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt new file mode 100644 index 0000000000..14dcbbefc4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class BlockedRelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getBlockedRelayListAddress() = BlockedRelayListEvent.createAddress(signer.pubKey) + + fun getBlockedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlockedRelayListAddress()) + + fun getBlockedRelayListFlow(): StateFlow = getBlockedRelayListNote().flow().metadata.stateFlow + + fun getBlockedRelayList(): BlockedRelayListEvent? = getBlockedRelayListNote().event as? BlockedRelayListEvent + + fun normalizeBlockedRelayListWithBackup(note: Note): Set { + val event = note.event as? BlockedRelayListEvent ?: settings.backupBlockedRelayList + return event?.relays()?.toSet() ?: emptySet() + } + + val flow = + getBlockedRelayListFlow() + .map { normalizeBlockedRelayListWithBackup(it.note) } + .onStart { emit(normalizeBlockedRelayListWithBackup(getBlockedRelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + fun saveRelayList( + blockedRelays: List, + onDone: (BlockedRelayListEvent) -> Unit, + ) { + val relayListForBlocked = getBlockedRelayList() + + if (relayListForBlocked != null && relayListForBlocked.tags.isNotEmpty()) { + BlockedRelayListEvent.updateRelayList( + earlierVersion = relayListForBlocked, + relays = blockedRelays, + signer = signer, + onReady = onDone, + ) + } else { + BlockedRelayListEvent.createFromScratch( + relays = blockedRelays, + signer = signer, + onReady = onDone, + ) + } + } + + init { + settings.backupBlockedRelayList?.let { + Log.d("AccountRegisterObservers", "Loading saved Blocked relay list ${it.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Blocked Relay List Collector Start") + getBlockedRelayListFlow().collect { + Log.d("AccountRegisterObservers", "Updating Blocked Relay List for ${signer.pubKey}") + (it.note.event as? BlockedRelayListEvent)?.let { + settings.updateBlockedRelayList(it) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt new file mode 100644 index 0000000000..dc611b54fc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists + +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +class TrustedRelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getTrustedRelayListAddress() = TrustedRelayListEvent.createAddress(signer.pubKey) + + fun getTrustedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getTrustedRelayListAddress()) + + fun getTrustedRelayListFlow(): StateFlow = getTrustedRelayListNote().flow().metadata.stateFlow + + fun getTrustedRelayList(): TrustedRelayListEvent? = getTrustedRelayListNote().event as? TrustedRelayListEvent + + fun normalizeTrustedRelayListWithBackup(note: Note): Set { + val event = note.event as? TrustedRelayListEvent ?: settings.backupTrustedRelayList + return event?.relays()?.toSet() ?: emptySet() + } + + val flow = + getTrustedRelayListFlow() + .map { normalizeTrustedRelayListWithBackup(it.note) } + .onStart { emit(normalizeTrustedRelayListWithBackup(getTrustedRelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + fun saveRelayList( + trustedRelays: List, + onDone: (TrustedRelayListEvent) -> Unit, + ) { + val relayListForTrusted = getTrustedRelayList() + + if (relayListForTrusted != null && relayListForTrusted.tags.isNotEmpty()) { + TrustedRelayListEvent.updateRelayList( + earlierVersion = relayListForTrusted, + relays = trustedRelays, + signer = signer, + onReady = onDone, + ) + } else { + TrustedRelayListEvent.createFromScratch( + relays = trustedRelays, + signer = signer, + onReady = onDone, + ) + } + } + + init { + settings.backupTrustedRelayList?.let { + Log.d("AccountRegisterObservers", "Loading saved Trusted relay list ${it.toJson()}") + @OptIn(DelicateCoroutinesApi::class) + GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + } + + scope.launch(Dispatchers.Default) { + Log.d("AccountRegisterObservers", "Trusted Relay List Collector Start") + getTrustedRelayListFlow().collect { + Log.d("AccountRegisterObservers", "Updating Trusted Relay List for ${signer.pubKey}") + (it.note.event as? TrustedRelayListEvent)?.let { + settings.updateTrustedRelayList(it) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt index ba3a5984ce..e0bf5faa0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope @@ -41,32 +42,33 @@ class TrustedRelayListsState( val localRelayList: LocalRelayListState, val dmRelayList: DmRelayListState, val searchRelayListState: SearchRelayListState, + val trustedRelayList: TrustedRelayListState, val scope: CoroutineScope, ) { - fun mergeLists( - nip65: Set, - private: Set, - local: Set, - dm: Set, - search: Set, - ): Set = nip65 + private + local + dm + search + fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } val flow: StateFlow> = combine( - nip65RelayList.allFlow, - privateOutboxRelayList.flow, - localRelayList.flow, - dmRelayList.flow, - searchRelayListState.flow, + listOf( + nip65RelayList.allFlow, + privateOutboxRelayList.flow, + localRelayList.flow, + dmRelayList.flow, + searchRelayListState.flow, + trustedRelayList.flow, + ), ::mergeLists, ).onStart { emit( mergeLists( - nip65RelayList.allFlow.value, - privateOutboxRelayList.flow.value, - localRelayList.flow.value, - dmRelayList.flow.value, - searchRelayListState.flow.value, + arrayOf( + nip65RelayList.allFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + dmRelayList.flow.value, + searchRelayListState.flow.value, + trustedRelayList.flow.value, + ), ), ) }.flowOn(Dispatchers.Default) @@ -74,11 +76,14 @@ class TrustedRelayListsState( scope, SharingStarted.Eagerly, mergeLists( - nip65RelayList.allFlow.value, - privateOutboxRelayList.flow.value, - localRelayList.flow.value, - dmRelayList.flow.value, - searchRelayListState.flow.value, + arrayOf( + nip65RelayList.allFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + dmRelayList.flow.value, + searchRelayListState.flow.value, + trustedRelayList.flow.value, + ), ), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index ed0b9bccd9..dbaf694a0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -50,18 +50,19 @@ class FeedTopNavFilterState( val allFollows: StateFlow, val locationFlow: StateFlow, val followsRelays: StateFlow>, + val blockedRelays: StateFlow>, val signer: NostrSigner, val scope: CoroutineScope, ) { fun loadFlowsFor(listName: String): IFeedFlowsType = when (listName) { GLOBAL_FOLLOWS -> GlobalFeedFlow(followsRelays) - ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays) + ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays, blockedRelays) AROUND_ME -> AroundMeFeedFlow(locationFlow, followsRelays) else -> { val note = LocalCache.checkGetOrCreateAddressableNote(listName) if (note != null) { - NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays) + NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays, blockedRelays) } else { UnknownFeedFlow(listName) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt index b840ce75df..79778a61dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt @@ -50,6 +50,7 @@ class AllFollowsByOutboxTopNavFilter( val geotags: Set? = null, val communities: Set? = null, val defaultRelays: StateFlow>, + val blockedRelays: StateFlow>, ) : IFeedTopNavFilter { val geotagScopes: Set? = geotags?.mapTo(mutableSetOf()) { GeohashId.Companion.toScope(it) } val hashtagScopes: Set? = hashtags?.mapTo(mutableSetOf()) { HashtagId.Companion.toScope(it) } @@ -91,8 +92,8 @@ class AllFollowsByOutboxTopNavFilter( MutableStateFlow(emptyMap()) } - return combine(authorsPerRelay, communitiesPerRelay, defaultRelays) { perRelayAuthors, perRelayCommunities, default -> - val allRelays = (perRelayAuthors.keys + perRelayCommunities.keys).ifEmpty { default } + return combine(authorsPerRelay, communitiesPerRelay, defaultRelays, blockedRelays) { perRelayAuthors, perRelayCommunities, default, blockedRelays -> + val allRelays = (perRelayAuthors.keys + perRelayCommunities.keys).filter { it !in blockedRelays }.ifEmpty { default } AllFollowsByOutboxTopNavPerRelayFilterSet( allRelays.associateWith { @@ -121,7 +122,7 @@ class AllFollowsByOutboxTopNavFilter( emptyMap() } - val allRelays = (authorsPerRelay.keys + communitiesPerRelay.keys).ifEmpty { defaultRelays.value } + val allRelays = (authorsPerRelay.keys + communitiesPerRelay.keys).filter { it !in blockedRelays.value }.ifEmpty { defaultRelays.value } return AllFollowsByOutboxTopNavPerRelayFilterSet( allRelays.associateWith { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt index e443909f54..112919801d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.map class AllFollowsFeedFlow( val allFollows: StateFlow, val followsRelays: StateFlow>, + val blockedRelays: StateFlow>, ) : IFeedFlowsType { fun convert(kind3: FollowListState.Kind3Follows?): AllFollowsByOutboxTopNavFilter = if (kind3 != null) { @@ -40,11 +41,13 @@ class AllFollowsFeedFlow( geotags = kind3.geotags, communities = kind3.communities, defaultRelays = followsRelays, + blockedRelays = blockedRelays, ) } else { AllFollowsByOutboxTopNavFilter( authors = emptySet(), defaultRelays = followsRelays, + blockedRelays = blockedRelays, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt index 71104c81ca..36b9672e4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt @@ -48,24 +48,25 @@ class NoteFeedFlow( val metadataFlow: StateFlow, val signer: NostrSigner, val allFollowRelays: StateFlow>, + val blockedRelays: StateFlow>, ) : IFeedFlowsType { fun process(noteEvent: Event): IFeedTopNavFilter = when (noteEvent) { is PeopleListEvent -> { if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { - MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users) + MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays) } else { - AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users) + AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays) } } is MuteListEvent -> { - MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users) + MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users, blockedRelays) } is FollowListEvent -> { - AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet()) + AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet(), blockedRelays) } is CommunityListEvent -> { - AllCommunitiesTopNavFilter(noteEvent.publicAndCachedPrivateCommunityIds().toSet()) + AllCommunitiesTopNavFilter(noteEvent.publicAndCachedPrivateCommunityIds().toSet(), blockedRelays) } is HashtagListEvent -> { HashtagTopNavFilter(noteEvent.publicAndCachedPrivateHashtags(), allFollowRelays) @@ -78,44 +79,45 @@ class NoteFeedFlow( community = noteEvent.addressTag(), authors = noteEvent.moderatorKeys().toSet().ifEmpty { null }, relays = noteEvent.relayUrls().toSet(), + blockedRelays = blockedRelays, ) } - else -> AuthorsByOutboxTopNavFilter(emptySet()) + else -> AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays) } suspend fun FlowCollector.process(noteEvent: Event) { when (noteEvent) { is PeopleListEvent -> { if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { - emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users)) + emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays)) noteEvent.publicAndPrivateUsersAndWords(signer)?.let { - emit(MutedAuthorsByOutboxTopNavFilter(it.users)) + emit(MutedAuthorsByOutboxTopNavFilter(it.users, blockedRelays)) } } else { - emit(AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users)) + emit(AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays)) noteEvent.publicAndPrivateUsersAndWords(signer)?.let { - emit(AuthorsByOutboxTopNavFilter(it.users)) + emit(AuthorsByOutboxTopNavFilter(it.users, blockedRelays)) } } } is MuteListEvent -> { - emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users)) + emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users, blockedRelays)) noteEvent.publicAndPrivateUsersAndWords(signer)?.let { - emit(MutedAuthorsByOutboxTopNavFilter(it.users)) + emit(MutedAuthorsByOutboxTopNavFilter(it.users, blockedRelays)) } } is FollowListEvent -> { - emit(AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet())) + emit(AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet(), blockedRelays)) } is CommunityListEvent -> { - emit(AllCommunitiesTopNavFilter(noteEvent.publicCommunityIds().toSet())) + emit(AllCommunitiesTopNavFilter(noteEvent.publicCommunityIds().toSet(), blockedRelays)) noteEvent.publicAndPrivateCommunities(signer)?.let { val communities = it.map { it.addressId }.toSet() - emit(AllCommunitiesTopNavFilter(communities)) + emit(AllCommunitiesTopNavFilter(communities, blockedRelays)) } } is HashtagListEvent -> { @@ -138,12 +140,13 @@ class NoteFeedFlow( community = noteEvent.addressTag(), authors = noteEvent.moderatorKeys().toSet().ifEmpty { null }, relays = noteEvent.relayUrls().toSet(), + blockedRelays = blockedRelays, ), ) } else -> emit( - AuthorsByOutboxTopNavFilter(emptySet()), + AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays), ) } } @@ -153,7 +156,7 @@ class NoteFeedFlow( metadataFlow.transformLatest { noteState -> val noteEvent = noteState?.note?.event if (noteEvent == null) { - AuthorsByOutboxTopNavFilter(emptySet()) + AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays) } else { process(noteEvent) } @@ -162,7 +165,7 @@ class NoteFeedFlow( override fun startValue(): IFeedTopNavFilter { val noteEvent = metadataFlow.value?.note?.event if (noteEvent == null) { - return AuthorsByOutboxTopNavFilter(emptySet()) + return AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays) } else { return process(noteEvent) } @@ -171,7 +174,7 @@ class NoteFeedFlow( override suspend fun startValue(collector: FlowCollector) { val noteEvent = metadataFlow.value?.note?.event if (noteEvent == null) { - collector.emit(AuthorsByOutboxTopNavFilter(emptySet())) + collector.emit(AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays)) } else { collector.process(noteEvent) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt index 741ef90688..57df0edcc9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt @@ -29,10 +29,14 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNotes import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlin.collections.minus @Immutable class AllCommunitiesTopNavFilter( val communities: Set, + val blockedRelays: StateFlow>, ) : IFeedTopNavFilter { override fun matchAuthor(pubkey: HexKey): Boolean = true @@ -43,13 +47,17 @@ class AllCommunitiesTopNavFilter( map.mapValues { AllCommunitiesTopNavPerRelayFilter(it.value) }, ) - override fun toPerRelayFlow(cache: LocalCache): Flow = - CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) { - convert(it) - } + override fun toPerRelayFlow(cache: LocalCache): Flow { + val communitiesPerRelay = CommunityRelayLoader.toCommunitiesPerRelayFlow(communities, cache) { it } - override fun startValue(cache: LocalCache): AllCommunitiesTopNavPerRelayFilterSet = - CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) { - convert(it) + return combine(communitiesPerRelay, blockedRelays) { communitiesPerRelay, blockedRelays -> + convert(communitiesPerRelay.minus(blockedRelays)) } + } + + override fun startValue(cache: LocalCache): AllCommunitiesTopNavPerRelayFilterSet { + val communitiesPerRelay = CommunityRelayLoader.communitiesPerRelaySnapshot(communities, cache) { it } + + return convert(communitiesPerRelay.minus(blockedRelays.value)) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt index e6b2e5c1ce..1b65e529fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt @@ -29,10 +29,13 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine @Immutable class AuthorsByOutboxTopNavFilter( val authors: Set, + val blockedRelays: StateFlow>, ) : IFeedTopNavFilter { override fun matchAuthor(pubkey: HexKey) = pubkey in authors @@ -48,7 +51,17 @@ class AuthorsByOutboxTopNavFilter( map.mapValues { AuthorsByOutboxTopNavPerRelayFilter(it.value) }, ) - override fun toPerRelayFlow(cache: LocalCache): Flow = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert) + override fun toPerRelayFlow(cache: LocalCache): Flow { + val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } - override fun startValue(cache: LocalCache): AuthorsByOutboxTopNavPerRelayFilterSet = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert) + return combine(authorsPerRelay, blockedRelays) { authors, blocked -> + convert(authors.minus(blocked)) + } + } + + override fun startValue(cache: LocalCache): AuthorsByOutboxTopNavPerRelayFilterSet { + val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } + + return convert(authorsPerRelay.minus(blockedRelays.value)) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt index 273d373b80..7b090f141e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/community/SingleCommunityTopNavFilter.kt @@ -31,13 +31,16 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableN import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map @Immutable class SingleCommunityTopNavFilter( val community: String, val authors: Set?, val relays: Set, + val blockedRelays: StateFlow>, ) : IFeedTopNavFilter { override fun matchAuthor(pubkey: HexKey) = authors == null || pubkey in authors @@ -53,20 +56,22 @@ class SingleCommunityTopNavFilter( override fun toPerRelayFlow(cache: LocalCache): Flow { // relay field takes priority if (relays.isNotEmpty()) { - return MutableStateFlow( + return blockedRelays.map { blocked -> SingleCommunityTopNavPerRelayFilterSet( - relays.associateWith { + relays.minus(blocked).associateWith { SingleCommunityTopNavPerRelayFilter(community, authors) }, - ), - ) + ) + } } if (authors != null) { // go by authors - return OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { + val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } + + return combine(authorsPerRelay, blockedRelays) { authorsPerRelay, blocked -> SingleCommunityTopNavPerRelayFilterSet( - it.mapValues { + authorsPerRelay.minus(blocked).mapValues { SingleCommunityTopNavPerRelayFilter(community, it.value) }, ) @@ -74,20 +79,20 @@ class SingleCommunityTopNavFilter( } // go by hints - return MutableStateFlow( + return blockedRelays.map { blocked -> SingleCommunityTopNavPerRelayFilterSet( - cache.relayHints.hintsForAddress(community).associateWith { + cache.relayHints.hintsForAddress(community).minus(blocked).associateWith { SingleCommunityTopNavPerRelayFilter(community, authors) }, - ), - ) + ) + } } override fun startValue(cache: LocalCache): SingleCommunityTopNavPerRelayFilterSet { // relay field takes priority if (relays.isNotEmpty()) { return SingleCommunityTopNavPerRelayFilterSet( - relays.associateWith { + relays.minus(blockedRelays.value).associateWith { SingleCommunityTopNavPerRelayFilter(community, authors) }, ) @@ -95,18 +100,18 @@ class SingleCommunityTopNavFilter( if (authors != null) { // go by authors - return OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { - SingleCommunityTopNavPerRelayFilterSet( - it.mapValues { - SingleCommunityTopNavPerRelayFilter(community, it.value) - }, - ) - } + val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } + + return SingleCommunityTopNavPerRelayFilterSet( + authorsPerRelay.minus(blockedRelays.value).mapValues { + SingleCommunityTopNavPerRelayFilter(community, it.value) + }, + ) } // go by hints return SingleCommunityTopNavPerRelayFilterSet( - cache.relayHints.hintsForAddress(community).associateWith { + cache.relayHints.hintsForAddress(community).minus(blockedRelays.value).associateWith { SingleCommunityTopNavPerRelayFilter(community, authors) }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt index d90db485db..d1bd98be0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt @@ -29,10 +29,13 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine @Immutable class MutedAuthorsByOutboxTopNavFilter( val authors: Set, + val blockedRelays: StateFlow>, ) : IFeedTopNavFilter { override fun matchAuthor(pubkey: HexKey) = pubkey in authors @@ -48,7 +51,17 @@ class MutedAuthorsByOutboxTopNavFilter( map.mapValues { MutedAuthorsByOutboxTopNavPerRelayFilter(it.value) }, ) - override fun toPerRelayFlow(cache: LocalCache): Flow = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache, ::convert) + override fun toPerRelayFlow(cache: LocalCache): Flow { + val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } - override fun startValue(cache: LocalCache): MutedAuthorsByOutboxTopNavPerRelayFilterSet = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache, ::convert) + return combine(authorsPerRelay, blockedRelays) { authors, blocked -> + convert(authors.minus(blocked)) + } + } + + override fun startValue(cache: LocalCache): MutedAuthorsByOutboxTopNavPerRelayFilterSet { + val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } + + return convert(authorsPerRelay.minus(blockedRelays.value)) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 8909f2b978..13e0c59e5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -31,6 +31,8 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent @@ -49,6 +51,12 @@ val AccountInfoAndListsFromKeyKinds = PrivateOutboxRelayListEvent.KIND, ) +val AccountInfoAndListsFromKeyKinds2 = + listOf( + BlockedRelayListEvent.KIND, + TrustedRelayListEvent.KIND, + ) + val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND) val AmethystMetadataTagMapFilter = mapOf("d" to listOf(APP_SPECIFIC_DATA_D_TAG)) @@ -70,6 +78,16 @@ fun filterAccountInfoAndListsFromKey( since = since, ), ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = AccountInfoAndListsFromKeyKinds2, + authors = listOf(pubkey), + limit = 20, + since = since, + ), + ), RelayBasedFilter( relay = relay, filter = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt index 3ab934bd29..7e65c20e4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -28,6 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent @@ -41,6 +43,8 @@ val BasicAccountInfoKinds = SearchRelayListEvent.KIND, FileServersEvent.KIND, BlossomServersEvent.KIND, + BlockedRelayListEvent.KIND, + TrustedRelayListEvent.KIND, ) fun filterBasicAccountInfoFromKeys( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 53d3312a7b..bc1c2bc362 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -127,7 +127,7 @@ fun AppNavigation( composableFromEnd { SettingsScreen(sharedPreferencesViewModel, accountViewModel, nav) } composableFromEnd { UserSettingsScreen(accountViewModel, nav) } composableFromBottomArgs { NIP47SetupScreen(accountViewModel, nav, it.nip47) } - composableFromEndArgs { AllRelayListScreen(it.toAdd, accountViewModel, nav) } + composableFromEndArgs { AllRelayListScreen(accountViewModel, nav) } composableFromEndArgs { DvmContentDiscoveryScreen(it.id, accountViewModel, nav) } composableFromEndArgs { ProfileScreen(it.id, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 7799af097b..21504cd17b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -455,7 +455,7 @@ fun ListContent( accountViewModel = accountViewModel, onClick = { nav.closeDrawer() - nav.nav(Route.EditRelays()) + nav.nav(Route.EditRelays) }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 6f904d6755..8a7a58c0ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -53,9 +53,7 @@ sealed class Route { @Serializable object EditProfile : Route() - @Serializable data class EditRelays( - val toAdd: String? = null, - ) : Route() + @Serializable object EditRelays : Route() @Serializable data class Nip47NWCSetup( val nip47: String? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index c55ea2a92d..3c7c1a249d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -78,12 +78,14 @@ import com.vitorpamplona.amethyst.ui.note.elements.Reward import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay +import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet import com.vitorpamplona.amethyst.ui.note.types.DisplaySearchRelayList +import com.vitorpamplona.amethyst.ui.note.types.DisplayTrustedRelayList import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.note.types.EmptyState import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay @@ -181,10 +183,12 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent +import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent @@ -635,6 +639,8 @@ private fun RenderNoteRow( is ChatMessageRelayListEvent -> DisplayDMRelayList(baseNote, backgroundColor, accountViewModel, nav) is AdvertisedRelayListEvent -> DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav) is SearchRelayListEvent -> DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav) + is BlockedRelayListEvent -> DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) + is TrustedRelayListEvent -> DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) is PinListEvent -> RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav) is EmojiPackEvent -> RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel) is LiveActivitiesEvent -> RenderLiveActivityEvent(baseNote, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index 1f03df636d..15d3b65508 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -37,6 +37,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -57,7 +59,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent +import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -188,6 +192,58 @@ fun DisplaySearchRelayList( ) } +@Composable +fun DisplayBlockedRelayList( + baseNote: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = baseNote.event as? BlockedRelayListEvent ?: return + + val relays by + remember(baseNote) { + mutableStateOf( + noteEvent.relays().toImmutableList(), + ) + } + + DisplayRelaySet( + relays, + stringRes(id = R.string.blocked_relays_title), + null, + backgroundColor, + accountViewModel, + nav, + ) +} + +@Composable +fun DisplayTrustedRelayList( + baseNote: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = baseNote.event as? TrustedRelayListEvent ?: return + + val relays by + remember(baseNote) { + mutableStateOf( + noteEvent.relays().toImmutableList(), + ) + } + + DisplayRelaySet( + relays, + stringRes(id = R.string.trusted_relays_title), + null, + backgroundColor, + accountViewModel, + nav, + ) +} + @Composable fun DisplayRelaySet( relays: ImmutableList, @@ -280,14 +336,16 @@ private fun RelayOptionsAction( nav: INav, ) { val isCurrentlyOnTheUsersList by observeUserRelayIntoList(accountViewModel.userProfile(), relay, accountViewModel) + val clipboardManager = LocalClipboardManager.current if (isCurrentlyOnTheUsersList) { AddRelayButton { - nav.nav(Route.EditRelays(relay.url)) + clipboardManager.setText(AnnotatedString(relay.url)) + nav.nav(Route.EditRelays) } } else { RemoveRelayButton { - nav.nav(Route.EditRelays(relay.url)) + nav.nav(Route.EditRelays) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt index 8d3595b267..7723ed123b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt @@ -56,7 +56,7 @@ class FollowingEphemeralChatSubAssembler( userJobMap[key.account.userProfile()] = listOf( key.account.scope.launch(Dispatchers.Default) { - key.account.ephemeralChatList.liveEphemeralChatList.sample(5000).collectLatest { + key.account.ephemeralChatList.liveEphemeralChatList.sample(500).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt index 61b3de874f..fdbfef14dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt @@ -57,7 +57,7 @@ class FollowingPublicChatSubAssembler( userJobMap[key.account.userProfile()] = listOf( key.account.scope.launch(Dispatchers.Default) { - key.account.publicChatList.flowSet.sample(5000).collectLatest { + key.account.publicChatList.flowSet.sample(500).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt index 52464b0512..86c065b858 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt @@ -77,7 +77,7 @@ class DiscoveryFollowsDiscoverySubAssembler1( } }, key.scope.launch(Dispatchers.Default) { - key.followsPerRelayFlow().sample(5000).collectLatest { + key.followsPerRelayFlow().sample(500).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt index 922385f2e0..4a4cd20686 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt @@ -75,7 +75,7 @@ class DiscoveryFollowsDiscoverySubAssembler2( } }, key.scope.launch(Dispatchers.Default) { - key.followsPerRelayFlow().sample(5000).collectLatest { + key.followsPerRelayFlow().sample(500).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt index 0c9d240abc..97d6941913 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt @@ -75,7 +75,7 @@ class DiscoveryFollowsDiscoverySubAssembler3( } }, key.scope.launch(Dispatchers.Default) { - key.followsPerRelayFlow().sample(5000).collectLatest { + key.followsPerRelayFlow().sample(500).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt index 42281203d1..3076df88d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt @@ -26,6 +26,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.navigation.INav @@ -46,6 +48,7 @@ fun RelayFeedView( RefresheableBox(viewModel, enablePullRefresh) { val listState = rememberLazyListState() + val clipboardManager = LocalClipboardManager.current LazyColumn( contentPadding = FeedPadding, @@ -56,10 +59,11 @@ fun RelayFeedView( item, accountViewModel = accountViewModel, onAddRelay = { - nav.nav(Route.EditRelays(item.url.url)) + clipboardManager.setText(AnnotatedString(item.url.url)) + nav.nav(Route.EditRelays) }, onRemoveRelay = { - nav.nav(Route.EditRelays(item.url.url)) + nav.nav(Route.EditRelays) }, ) HorizontalDivider( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 68ae265cc1..d23aa6479d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -54,6 +54,8 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelayListViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems @@ -68,6 +70,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.renderNip65Hom import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.renderNip65NotifItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.renderSearchItems +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.TrustedRelayListViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.renderTrustedItems import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.MinHorzSpacer @@ -79,17 +83,15 @@ import com.vitorpamplona.amethyst.ui.theme.grayText @Composable fun AllRelayListScreen( - relayToAdd: String? = null, accountViewModel: AccountViewModel, nav: INav, ) { - MappedAllRelayListView(relayToAdd ?: "", accountViewModel, nav) + MappedAllRelayListView(accountViewModel, nav) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun MappedAllRelayListView( - relayToAdd: String = "", accountViewModel: AccountViewModel, newNav: INav, ) { @@ -106,6 +108,12 @@ fun MappedAllRelayListView( val searchViewModel: SearchRelayListViewModel = viewModel() val searchFeedState by searchViewModel.relays.collectAsStateWithLifecycle() + val blockedViewModel: BlockedRelayListViewModel = viewModel() + val blockedFeedState by blockedViewModel.relays.collectAsStateWithLifecycle() + + val trustedViewModel: TrustedRelayListViewModel = viewModel() + val trustedFeedState by trustedViewModel.relays.collectAsStateWithLifecycle() + val localViewModel: LocalRelayListViewModel = viewModel() val localFeedState by localViewModel.relays.collectAsStateWithLifecycle() @@ -119,6 +127,8 @@ fun MappedAllRelayListView( localViewModel.load(accountViewModel.account) privateOutboxViewModel.load(accountViewModel.account) connectedViewModel.load(accountViewModel.account) + blockedViewModel.load(accountViewModel.account) + trustedViewModel.load(accountViewModel.account) } Scaffold( @@ -148,6 +158,8 @@ fun MappedAllRelayListView( searchViewModel.create() localViewModel.create() privateOutboxViewModel.create() + trustedViewModel.create() + blockedViewModel.create() newNav.popBack() }, true, @@ -164,6 +176,8 @@ fun MappedAllRelayListView( searchViewModel.clear() localViewModel.clear() privateOutboxViewModel.clear() + trustedViewModel.clear() + blockedViewModel.clear() newNav.popBack() }, ) @@ -248,6 +262,24 @@ fun MappedAllRelayListView( } renderLocalItems(localFeedState, localViewModel, accountViewModel, newNav) + item { + SettingsCategory( + stringRes(R.string.trusted_section), + stringRes(R.string.trusted_section_explainer), + SettingsCategorySpacingModifier, + ) + } + renderTrustedItems(trustedFeedState, trustedViewModel, accountViewModel, newNav) + + item { + SettingsCategory( + stringRes(R.string.blocked_section), + stringRes(R.string.blocked_section_explainer), + SettingsCategorySpacingModifier, + ) + } + renderBlockedItems(blockedFeedState, blockedViewModel, accountViewModel, newNav) + item { SettingsCategory( stringRes(R.string.connected_section), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt new file mode 100644 index 0000000000..05169ebab5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer + +@Composable +fun BlockedRelayList( + postViewModel: BlockedRelayListViewModel, + accountViewModel: AccountViewModel, + onClose: () -> Unit, + nav: INav, +) { + val feedState by postViewModel.relays.collectAsStateWithLifecycle() + + val newNav = rememberExtendedNav(nav, onClose) + + Row(verticalAlignment = Alignment.CenterVertically) { + LazyColumn( + contentPadding = FeedPadding, + ) { + renderBlockedItems(feedState, postViewModel, accountViewModel, newNav) + } + } +} + +fun LazyListScope.renderBlockedItems( + feedState: List, + postViewModel: BlockedRelayListViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + itemsIndexed(feedState, key = { _, item -> "Trusted" + item.relay }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + accountViewModel = accountViewModel, + nav, + ) + } + + item { + Spacer(modifier = StdVertSpacer) + RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt new file mode 100644 index 0000000000..a0bb1fa64b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class BlockedRelayListViewModel : BasicRelaySetupInfoModel() { + override fun getRelayList(): List? = account.blockedRelayList.getBlockedRelayList()?.relays() + + override fun saveRelayList(urlList: List) { + account.saveBlockedRelayList(urlList) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt new file mode 100644 index 0000000000..1b9d872f88 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer + +@Composable +fun TrustedRelayList( + postViewModel: TrustedRelayListViewModel, + accountViewModel: AccountViewModel, + onClose: () -> Unit, + nav: INav, +) { + val feedState by postViewModel.relays.collectAsStateWithLifecycle() + + val newNav = rememberExtendedNav(nav, onClose) + + Row(verticalAlignment = Alignment.CenterVertically) { + LazyColumn( + contentPadding = FeedPadding, + ) { + renderTrustedItems(feedState, postViewModel, accountViewModel, newNav) + } + } +} + +fun LazyListScope.renderTrustedItems( + feedState: List, + postViewModel: TrustedRelayListViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + itemsIndexed(feedState, key = { _, item -> "Trusted" + item.relay }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + accountViewModel = accountViewModel, + nav, + ) + } + + item { + Spacer(modifier = StdVertSpacer) + RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt new file mode 100644 index 0000000000..6c3e4600f0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class TrustedRelayListViewModel : BasicRelaySetupInfoModel() { + override fun getRelayList(): List? = account.trustedRelayList.getTrustedRelayList()?.relays() + + override fun saveRelayList(urlList: List) { + account.saveTrustedRelayList(urlList) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index f4a333bdac..ba91a76684 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -117,12 +117,14 @@ import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.types.AudioHeader import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay +import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet import com.vitorpamplona.amethyst.ui.note.types.DisplaySearchRelayList +import com.vitorpamplona.amethyst.ui.note.types.DisplayTrustedRelayList import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay @@ -201,10 +203,12 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent +import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup @@ -589,6 +593,10 @@ private fun FullBleedNoteCompose( DisplayNIP65RelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is SearchRelayListEvent) { DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav) + } else if (noteEvent is BlockedRelayListEvent) { + DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) + } else if (noteEvent is TrustedRelayListEvent) { + DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is FhirResourceEvent) { RenderFhirResource(baseNote, accountViewModel, nav) } else if (noteEvent is GitRepositoryEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt index cc5787e9b3..9d0311ad3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt @@ -92,7 +92,7 @@ class VideoOutboxEventsFilterSubAssembler( userJobMap[user] = listOf( key.scope.launch(Dispatchers.Default) { - key.followsPerRelayFlow().sample(5000).collectLatest { + key.followsPerRelayFlow().sample(500).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index bac3065b01..e7083237cc 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1052,6 +1052,9 @@ Insert between 1–3 relays to use when searching for content or tagging users. Make sure your chosen relays implement NIP-50 Good options are:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com + Blocked Relays + Trusted Relays + DM Upload Relay Settings Public Outbox/Home Relays @@ -1076,6 +1079,12 @@ Local Relays List of relays that are running in this device. + Trusted Relays + Relays you trust to not need a Tor connection for + + Blocked Relays + Amethyst will never connect to these relays + Zap the Devs! Your donation helps us make a difference. Every sat counts! Donate Now diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 6fd03794e0..acf2c7df07 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -74,12 +74,14 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent +import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -145,6 +147,7 @@ class EventFactory { BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig) BadgeDefinitionEvent.KIND -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig) BadgeProfilesEvent.KIND -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig) + BlockedRelayListEvent.KIND -> BlockedRelayListEvent(id, pubKey, createdAt, tags, content, sig) BlossomServersEvent.KIND -> BlossomServersEvent(id, pubKey, createdAt, tags, content, sig) BlossomAuthorizationEvent.KIND -> BlossomAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig) @@ -257,6 +260,7 @@ class EventFactory { TextNoteModificationEvent.KIND -> TextNoteModificationEvent(id, pubKey, createdAt, tags, content, sig) TorrentEvent.KIND -> TorrentEvent(id, pubKey, createdAt, tags, content, sig) TorrentCommentEvent.KIND -> TorrentCommentEvent(id, pubKey, createdAt, tags, content, sig) + TrustedRelayListEvent.KIND -> TrustedRelayListEvent(id, pubKey, createdAt, tags, content, sig) VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig) VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig) WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index ea512b6caa..a90876b998 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart @@ -70,9 +71,10 @@ class NostrClient( reqs + counts + outbox }.onStart { activeRequests.relays.value + activeCounts.relays.value + eventOutbox.relays.value - }.onEach { - relayPool.updatePool(it) - }.flowOn(Dispatchers.Default) + }.debounce(300) + .onEach { + relayPool.updatePool(it) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Companion.Eagerly, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BlockedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BlockedRelayListEvent.kt new file mode 100644 index 0000000000..12c371db2e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BlockedRelayListEvent.kt @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip50Search.tags.RelayTag +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class BlockedRelayListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun relays(): List = tags.mapNotNull(RelayTag::parse) + + companion object { + const val KIND = 10006 + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) + + fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) + + fun createTagArray(relays: List): Array> = + relays + .map { + RelayTag.assemble(it) + }.plusElement(AltTag.assemble("Relay list to use for trusted connections")) + .toTypedArray() + + fun updateRelayList( + earlierVersion: BlockedRelayListEvent, + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BlockedRelayListEvent) -> Unit, + ) { + val tags = + earlierVersion.tags + .filter(RelayTag::notMatch) + .plus( + relays.map { + RelayTag.assemble(it) + }, + ).toTypedArray() + + signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady) + } + + fun createFromScratch( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BlockedRelayListEvent) -> Unit, + ) { + create(relays, signer, createdAt, onReady) + } + + fun create( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (BlockedRelayListEvent) -> Unit, + ) { + signer.sign(createdAt, KIND, createTagArray(relays), "", onReady) + } + + fun create( + relays: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): BlockedRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "") + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TrustedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TrustedRelayListEvent.kt new file mode 100644 index 0000000000..22436fb974 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TrustedRelayListEvent.kt @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip50Search.tags.RelayTag +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class TrustedRelayListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun relays(): List = tags.mapNotNull(RelayTag::parse) + + companion object { + const val KIND = 10089 + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) + + fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) + + fun createTagArray(relays: List): Array> = + relays + .map { + RelayTag.assemble(it) + }.plusElement(AltTag.assemble("Relay list to use for trusted connections")) + .toTypedArray() + + fun updateRelayList( + earlierVersion: TrustedRelayListEvent, + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (TrustedRelayListEvent) -> Unit, + ) { + val tags = + earlierVersion.tags + .filter(RelayTag::notMatch) + .plus( + relays.map { + RelayTag.assemble(it) + }, + ).toTypedArray() + + signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady) + } + + fun createFromScratch( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (TrustedRelayListEvent) -> Unit, + ) { + create(relays, signer, createdAt, onReady) + } + + fun create( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (TrustedRelayListEvent) -> Unit, + ) { + signer.sign(createdAt, KIND, createTagArray(relays), "", onReady) + } + + fun create( + relays: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): TrustedRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "") + } +} From 0fe8f6c78ced0ae14ba8ada552ae08c83e534f1f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 7 Jul 2025 20:40:29 -0400 Subject: [PATCH 041/185] Improves debouncing when changing relays. --- .../quartz/nip01Core/relay/client/NostrClient.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index a90876b998..a0641bd826 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -36,12 +36,13 @@ import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.stateIn /** @@ -62,6 +63,7 @@ class NostrClient( * Whatches for any changes in the relay list from subscriptions or outbox * and updates the relayPool as needed. */ + @OptIn(FlowPreview::class) private val allRelays = combine( activeRequests.relays, @@ -71,7 +73,7 @@ class NostrClient( reqs + counts + outbox }.onStart { activeRequests.relays.value + activeCounts.relays.value + eventOutbox.relays.value - }.debounce(300) + }.sample(300) .onEach { relayPool.updatePool(it) }.flowOn(Dispatchers.Default) From 2c90aa4b1eb82d8652b1bb0c2472b23d3db006c6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 08:54:13 -0400 Subject: [PATCH 042/185] Continues the migration to use full parsed Addresses instead of just addressIds on the Local Cache. Moves the community route to take a full address. --- .../amethyst/model/LocalCache.kt | 4 +-- .../vitorpamplona/amethyst/ui/MainActivity.kt | 2 +- .../amethyst/ui/navigation/AppNavigation.kt | 3 ++- .../amethyst/ui/navigation/RouteMaker.kt | 2 +- .../amethyst/ui/navigation/Routes.kt | 4 ++- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 26 +------------------ .../ui/note/UpdateReactionTypeDialog.kt | 5 ++-- .../note/elements/AddInboxRelayForDMCard.kt | 2 +- .../ui/note/elements/DisplayCommunity.kt | 2 +- .../amethyst/ui/screen/FollowListState.kt | 2 +- .../loggedIn/communities/CommunityScreen.kt | 13 +++++----- .../search/AddInboxRelayForSearchCard.kt | 2 +- 12 files changed, 23 insertions(+), 44 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index fccb6c74b1..0eb2d6a478 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -313,7 +313,7 @@ object LocalCache : ILocalCache { fun getAddressableNoteIfExists(key: String): AddressableNote? = Address.parse(key)?.let { addressables.get(it) } - fun getAddressableNoteIfExists(address: Address): AddressableNote? = getAddressableNoteIfExists(address) + fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address) fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) } @@ -2968,7 +2968,7 @@ object LocalCache : ILocalCache { if (event is AddressableEvent && relay != null) { // updates relay with a new event. - getAddressableNoteIfExists(event.addressTag())?.let { note -> + getAddressableNoteIfExists(event.address())?.let { note -> note.event?.let { existingEvent -> if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay.url)) { Log.d("LocalCache", "Updating ${relay.url.url} with a new version of ${event.toJson()} to ${existingEvent.toJson()}") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 50fb1a403b..5ec963e7b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -180,7 +180,7 @@ fun uriToRoute( is NAddress -> { if (nip19.kind == CommunityDefinitionEvent.KIND) { - Route.Community(nip19.aTag()) + Route.Community(nip19.kind, nip19.author, nip19.dTag) } else if (nip19.kind == LiveActivitiesEvent.KIND) { Route.Channel(nip19.aTag()) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index bc1c2bc362..181bcfd7ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -91,6 +91,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -134,7 +135,7 @@ fun AppNavigation( composableFromEndArgs { ThreadScreen(it.id, accountViewModel, nav) } composableFromEndArgs { HashtagScreen(it, accountViewModel, nav) } composableFromEndArgs { GeoHashScreen(it, accountViewModel, nav) } - composableFromEndArgs { CommunityScreen(it.id, accountViewModel, nav) } + composableFromEndArgs { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) } composableFromEndArgs { ChatroomScreen(it.id.toString(), it.message, it.replyId, it.draftId, accountViewModel, nav) } composableFromEndArgs { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) } composableFromEndArgs { ChannelScreen(it.id, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt index 13e608b3c5..c48c8d8341 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt @@ -102,7 +102,7 @@ fun routeFor( loggedIn.createChatroom(room) return Route.Room(room.hashCode()) } else if (noteEvent is CommunityDefinitionEvent) { - return Route.Community(noteEvent.aTag().toTag()) + return Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag()) } else if (noteEvent is AddressableEvent) { return Route.Note(noteEvent.aTag().toTag()) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 8a7a58c0ba..14caf81b94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -80,7 +80,9 @@ sealed class Route { ) : Route() @Serializable data class Community( - val id: String, + val kind: Int, + val pubKeyHex: HexKey, + val dTag: String, ) : Route() @Serializable data class Channel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index a1fba8d9dd..9fa960be45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -88,30 +88,6 @@ fun LoadDecryptedContentOrNull( inner(decryptedContent) } -@Composable -fun LoadAddressableNote( - aTagHex: String, - accountViewModel: AccountViewModel, - content: @Composable (AddressableNote?) -> Unit, -) { - var note by - remember(aTagHex) { - mutableStateOf(accountViewModel.getAddressableNoteIfExists(aTagHex)) - } - - if (note == null) { - LaunchedEffect(key1 = aTagHex) { - accountViewModel.checkGetOrCreateAddressableNote(aTagHex) { newNote -> - if (newNote != note) { - note = newNote - } - } - } - } - - content(note) -} - @Composable fun LoadAddressableNote( address: Address, @@ -120,7 +96,7 @@ fun LoadAddressableNote( ) { var note by remember(address) { - mutableStateOf(accountViewModel.getAddressableNoteIfExists(address.toValue())) + mutableStateOf(accountViewModel.getAddressableNoteIfExists(address)) } if (note == null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index 96acdded67..1f4fa9947c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent @@ -386,7 +387,7 @@ private fun EmojiSelector( ) { emptyNote -> emptyNote?.let { usersEmojiList -> val collections by observeNoteEventAndMap(usersEmojiList, accountViewModel) { event: EmojiPackSelectionEvent -> - event.emojiPackIds().toImmutableList() + event.emojiPacks().toImmutableList() } collections?.let { EmojiCollectionGallery(it, accountViewModel, nav, onClick) } @@ -396,7 +397,7 @@ private fun EmojiSelector( @Composable fun EmojiCollectionGallery( - emojiCollections: ImmutableList, + emojiCollections: ImmutableList
, accountViewModel: AccountViewModel, nav: INav, onClick: ((EmojiUrlTag) -> Unit)? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt index fb58d929fd..c5ba242ba3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt @@ -102,7 +102,7 @@ fun ObserveRelayListForDMs( inner: @Composable (relayListEvent: ChatMessageRelayListEvent?) -> Unit, ) { LoadAddressableNote( - ChatMessageRelayListEvent.createAddressTag(pubkey), + ChatMessageRelayListEvent.createAddress(pubkey), accountViewModel, ) { relayList -> if (relayList != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt index 2b56d62505..e27ef8051f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt @@ -59,7 +59,7 @@ private fun DisplayCommunity( val displayTag = remember(note) { - buildLinkString(getCommunityShortName(communityTag)) { nav.nav(Route.Community(communityTag.toTag())) } + buildLinkString(getCommunityShortName(communityTag)) { nav.nav(Route.Community(communityTag.kind, communityTag.pubKeyHex, communityTag.dTag)) } } Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt index 30e4f432c9..aec8b10241 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt @@ -170,7 +170,7 @@ class FollowListState( "Community/${communityNote.idHex}", CommunityName(communityNote), CodeNameType.ROUTE, - route = Route.Community(communityNote.idHex), + route = Route.Community(communityNote.address.kind, communityNote.address.pubKeyHex, communityNote.address.dTag), kinds = DEFAULT_COMMUNITY_FEEDS, aTags = listOf(communityNote.idHex), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt index 35d3691a7d..665c55c89a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt @@ -42,16 +42,15 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.dal.CommunityFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.datasource.CommunityFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @Composable fun CommunityScreen( - aTagHex: String?, + aTagHex: Address, accountViewModel: AccountViewModel, nav: INav, ) { - if (aTagHex == null) return - - LoadAddressableNote(aTagHex = aTagHex, accountViewModel) { + LoadAddressableNote(aTagHex, accountViewModel) { it?.let { PrepareViewModelsCommunityScreen( note = it, @@ -94,7 +93,7 @@ fun CommunityScreen( DisappearingScaffold( isInvertedLayout = false, topBar = { - CommunityTopBar(note.idHex, accountViewModel, nav) + CommunityTopBar(note.address, accountViewModel, nav) }, floatingButton = { NewCommunityNoteButton(note.idHex, accountViewModel, nav) @@ -116,11 +115,11 @@ fun CommunityScreen( @Composable fun CommunityTopBar( - id: String, + id: Address, accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote(aTagHex = id, accountViewModel) { baseNote -> + LoadAddressableNote(id, accountViewModel) { baseNote -> if (baseNote != null) { TopBarExtensibleWithBackButton( title = { ShortCommunityHeader(baseNote, accountViewModel, nav) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt index 14b4f8c4a4..f2b92ebf21 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt @@ -101,7 +101,7 @@ fun ObserveRelayListForSearch( inner: @Composable (relayListEvent: SearchRelayListEvent?) -> Unit, ) { LoadAddressableNote( - SearchRelayListEvent.createAddressTag(pubkey), + SearchRelayListEvent.createAddress(pubkey), accountViewModel, ) { relayList -> if (relayList != null) { From 1b86c3d85b3835043d93aa32348e4d1c81143c7c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 09:22:33 -0400 Subject: [PATCH 043/185] Fixes the sending of multiple old events back to the relays that send old events to Amethyst. --- .../java/com/vitorpamplona/amethyst/model/LocalCache.kt | 6 +++++- .../src/main/java/com/vitorpamplona/amethyst/model/Note.kt | 2 +- .../nip01Core/relay/client/single/basic/BasicRelayClient.kt | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 0eb2d6a478..3b4dc9fd37 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2971,7 +2971,11 @@ object LocalCache : ILocalCache { getAddressableNoteIfExists(event.address())?.let { note -> note.event?.let { existingEvent -> if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay.url)) { - Log.d("LocalCache", "Updating ${relay.url.url} with a new version of ${event.toJson()} to ${existingEvent.toJson()}") + Log.d("LocalCache", "Updating ${relay.url.url} with a new version of ${event.kind} ${event.id} to ${existingEvent.id}") + + // only send once. + note.addRelay(relay.url) + relay.send(existingEvent) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 3cb6373292..eb2bb4f646 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -464,7 +464,7 @@ open class Note( } } - fun hasRelay(relay: NormalizedRelayUrl) = relay !in relays + fun hasRelay(relay: NormalizedRelayUrl) = relay in relays fun addRelay(relay: NormalizedRelayUrl) { if (relay !in relays) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 80bb399f45..fcfac8e720 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -263,7 +263,7 @@ open class BasicRelayClient( msg: OkMessage, onConnected: () -> Unit, ) { - // Log.w(logTag, "OK: ${msg.eventId} ${msg.success} ${msg.message}") + Log.w(logTag, "OK: ${msg.eventId} ${msg.success} ${msg.message}") // if this is the OK of an auth event, renew all subscriptions and resend all outgoing events. if (authResponseWatcher.containsKey(msg.eventId)) { From a05b2681ad880a9e12a0060f8a82018bd9fdb125 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 10:20:54 -0400 Subject: [PATCH 044/185] 1. Fixes crash when opening the relay selection dialog 2. Fixes color and size of ping labels on relays 3. Removes delete button for connected relay view. --- .../ui/actions/RelaySelectionDialog.kt | 8 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 4 +- .../amethyst/ui/note/RelayListRow.kt | 80 +++++++++++++++---- .../common/BasicRelaySetupInfoClickableRow.kt | 3 +- .../common/BasicRelaySetupInfoDialog.kt | 3 +- .../relays/common/RelayNameAndRemoveButton.kt | 32 ++++---- .../connected/ConnectedRelayListView.kt | 2 +- .../loggedIn/report/ReportNoteDialog.kt | 4 +- .../vitorpamplona/amethyst/ui/theme/Color.kt | 9 ++- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 6 ++ 10 files changed, 105 insertions(+), 46 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt index f95a045eee..0b737a14ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt @@ -61,7 +61,6 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl -import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlin.collections.map @@ -71,11 +70,6 @@ data class RelayList( val isSelected: Boolean, ) -data class RelayInfoDialog( - val relay: NormalizedRelayUrl, - val relayInfo: Nip11RelayInformation, -) - @OptIn(ExperimentalMaterial3Api::class) @Composable fun RelaySelectionDialog( @@ -156,7 +150,7 @@ fun RelaySelectionDialog( ) { itemsIndexed( relays, - key = { _, item -> item.relay }, + key = { _, item -> item.relay.url }, ) { index, item -> RenderRelaySwitch( item, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index f4652c99eb..ffc2c7ff93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -89,7 +89,7 @@ import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.WarningColor +import com.vitorpamplona.amethyst.ui.theme.LightRedColor import com.vitorpamplona.amethyst.ui.theme.isLight import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent @@ -480,7 +480,7 @@ private fun BlockAlertDialog( buttonText = stringRes(R.string.quick_action_block_dialog_btn), buttonColors = ButtonDefaults.buttonColors( - containerColor = WarningColor, + containerColor = LightRedColor, contentColor = Color.White, ), onClickDoOnce = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 5e5eb7d386..024faaaf74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -42,11 +43,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType @@ -59,15 +61,19 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size17dp import com.vitorpamplona.amethyst.ui.theme.StdStartPadding +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.ui.theme.allGoodColor import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.amethyst.ui.theme.redColorOnSecondSurface import com.vitorpamplona.amethyst.ui.theme.relayIconModifier import com.vitorpamplona.amethyst.ui.theme.ripple24dp +import com.vitorpamplona.amethyst.ui.theme.warningColorOnSecondSurface import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @Composable public fun RelayBadgesHorizontal( @@ -191,6 +197,39 @@ fun RenderRelay( } } +@Preview +@Composable +fun RenderRelayIconPreview() { + ThemeComparisonColumn { + Row { + RenderRelayIcon( + displayUrl = "wss://relay.damus.io", + iconUrl = "wss://relay.damus.io", + loadProfilePicture = true, + pingInMs = 100, + loadRobohash = true, + iconModifier = LargeRelayIconModifier, + ) + RenderRelayIcon( + displayUrl = "wss://relay.damus.io", + iconUrl = "wss://relay.damus.io", + loadProfilePicture = true, + pingInMs = 300, + loadRobohash = true, + iconModifier = LargeRelayIconModifier, + ) + RenderRelayIcon( + displayUrl = "wss://relay.damus.io", + iconUrl = "wss://relay.damus.io", + loadProfilePicture = true, + pingInMs = 500, + loadRobohash = true, + iconModifier = LargeRelayIconModifier, + ) + } + } +} + @Composable fun RenderRelayIcon( displayUrl: String, @@ -200,6 +239,9 @@ fun RenderRelayIcon( pingInMs: Long, iconModifier: Modifier = MaterialTheme.colorScheme.relayIconModifier, ) { + val green = MaterialTheme.colorScheme.allGoodColor + val yellow = MaterialTheme.colorScheme.warningColorOnSecondSurface + val red = MaterialTheme.colorScheme.redColorOnSecondSurface Box( contentAlignment = Alignment.TopEnd, ) { @@ -212,29 +254,35 @@ fun RenderRelayIcon( loadProfilePicture = loadProfilePicture, loadRobohash = loadRobohash, ) + + val textStyle = + remember(pingInMs) { + TextStyle( + color = + if (pingInMs <= 150) { + green + } else if (pingInMs <= 300) { + yellow + } else { + red + }, + ) + } + if (pingInMs > 0) { Box( modifier = Modifier - .clip(RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(6.dp)) .background( - Color.Gray, + MaterialTheme.colorScheme.secondaryContainer, ), ) { Text( - modifier = Modifier.padding(4.dp), - style = - TextStyle( - color = - if (pingInMs <= 150) { - Color.Green - } else if (pingInMs <= 300) { - Color.Yellow - } else { - Color.Red - }, - ), + modifier = Modifier.padding(3.dp), + style = textStyle, text = "$pingInMs", + fontSize = 10.sp, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt index 596b9562a9..a799bdf1ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common +import android.R.attr.onClick import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column @@ -50,7 +51,7 @@ fun BasicRelaySetupInfoClickableRow( item: BasicRelaySetupInfo, loadProfilePicture: Boolean, loadRobohash: Boolean, - onDelete: (BasicRelaySetupInfo) -> Unit, + onDelete: ((BasicRelaySetupInfo) -> Unit)?, onClick: () -> Unit, accountViewModel: AccountViewModel, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index fe247aac03..64c7fb1255 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.platform.LocalContext import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.service.Nip11Retriever +import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo @@ -38,7 +39,7 @@ import com.vitorpamplona.amethyst.ui.stringRes @Composable fun BasicRelaySetupInfoDialog( item: BasicRelaySetupInfo, - onDelete: (BasicRelaySetupInfo) -> Unit, + onDelete: ((BasicRelaySetupInfo) -> Unit)?, accountViewModel: AccountViewModel, nav: INav, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt index 86622eba91..a19a212359 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayNameAndRemoveButton.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.WarningColor +import com.vitorpamplona.amethyst.ui.theme.LightRedColor import com.vitorpamplona.amethyst.ui.theme.allGoodColor import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @@ -51,7 +51,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl fun RelayNameAndRemoveButton( item: BasicRelaySetupInfo, onClick: () -> Unit, - onDelete: (BasicRelaySetupInfo) -> Unit, + onDelete: ((BasicRelaySetupInfo) -> Unit)?, modifier: Modifier, ) { val clipboardManager = LocalClipboardManager.current @@ -95,19 +95,21 @@ fun RelayNameAndRemoveButton( } } - IconButton( - modifier = Modifier.size(30.dp), - onClick = { onDelete(item) }, - ) { - Icon( - imageVector = Icons.Default.Cancel, - contentDescription = stringRes(id = R.string.remove), - modifier = - Modifier - .padding(start = 10.dp) - .size(15.dp), - tint = WarningColor, - ) + if (onDelete != null) { + IconButton( + modifier = Modifier.size(30.dp), + onClick = { onDelete(item) }, + ) { + Icon( + imageVector = Icons.Default.Cancel, + contentDescription = stringRes(id = R.string.remove), + modifier = + Modifier + .padding(start = 10.dp) + .size(15.dp), + tint = LightRedColor, + ) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt index 300883116d..568d46f08d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt @@ -63,7 +63,7 @@ fun LazyListScope.renderConnectedItems( itemsIndexed(feedState, key = { _, item -> "Connected" + item.relay }) { index, item -> BasicRelaySetupInfoDialog( item, - onDelete = { }, + onDelete = null, accountViewModel = accountViewModel, nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt index ca85db9de1..663b21dd72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/report/ReportNoteDialog.kt @@ -66,7 +66,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.WarningColor +import com.vitorpamplona.amethyst.ui.theme.LightRedColor import com.vitorpamplona.quartz.nip56Reports.ReportType import kotlinx.collections.immutable.toImmutableList @@ -195,7 +195,7 @@ private fun ActionButton( ) = Button( onClick = onClick, enabled = enabled, - colors = ButtonDefaults.buttonColors(containerColor = WarningColor), + colors = ButtonDefaults.buttonColors(containerColor = LightRedColor), modifier = Modifier.fillMaxWidth(), ) { Row( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt index 66990bc7b1..f9acf35efc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Color.kt @@ -52,13 +52,20 @@ val Nip05EmailColorLight = Color(0xFFa770f3) val DarkerGreen = Color.Green.copy(alpha = 0.32f) -val WarningColor = Color(0xFFC62828) +val LightRedColor = Color(0xFFC62828) +val LighterRedColor = Color(0xFFFF0E0E) val RelayIconFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) }) val LightWarningColor = Color(0xFFffcc00) val DarkWarningColor = Color(0xFFF8DE22) +val LightRedColorOnSecondSurface = Color(0xFFC62828) +val DarkRedColorOnSecondSurface = Color(0xFFF34747) + +val LightWarningColorOnSecondSurface = Color(0xFFC09B14) +val DarkWarningColorOnSecondSurface = Color(0xFFE1C419) + val LightAllGoodColor = Color(0xFF339900) val DarkAllGoodColor = Color(0xFF99cc33) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 1970de0a03..39e062d22f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -391,9 +391,15 @@ val ColorScheme.overPictureBackground: Color val ColorScheme.bitcoinColor: Color get() = if (isLight) BitcoinLight else BitcoinDark +val ColorScheme.redColorOnSecondSurface: Color + get() = if (isLight) LightRedColorOnSecondSurface else DarkRedColorOnSecondSurface + val ColorScheme.warningColor: Color get() = if (isLight) LightWarningColor else DarkWarningColor +val ColorScheme.warningColorOnSecondSurface: Color + get() = if (isLight) LightWarningColorOnSecondSurface else DarkWarningColorOnSecondSurface + val ColorScheme.allGoodColor: Color get() = if (isLight) LightAllGoodColor else DarkAllGoodColor From 296fa201f764d0fdac5d16b8ded131f0a504facc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 11:28:30 -0400 Subject: [PATCH 045/185] Delete Relay Selection dialog from all new post screens. --- .../vitorpamplona/amethyst/model/Account.kt | 62 +++- .../amethyst/ui/actions/EditPostView.kt | 34 +-- .../amethyst/ui/actions/EditPostViewModel.kt | 15 +- .../amethyst/ui/actions/NewMediaModel.kt | 25 +- .../amethyst/ui/actions/NewMediaView.kt | 37 +-- .../ui/actions/NewPollVoteValueRange.kt | 4 +- .../ui/actions/RelaySelectionDialog.kt | 278 ------------------ .../nip22Comments/CommentPostViewModel.kt | 91 +++--- .../nip22Comments/GenericCommentPostScreen.kt | 30 -- .../nip99Classifieds/NewProductScreen.kt | 32 -- .../loggedIn/geohash/GeoHashPostScreen.kt | 1 - .../loggedIn/hashtag/HashtagPostScreen.kt | 1 - .../loggedIn/home/ShortNotePostScreen.kt | 34 +-- .../loggedIn/home/ShortNotePostViewModel.kt | 148 +++++----- .../nip01Core/relay/client/NostrClient.kt | 10 +- 15 files changed, 185 insertions(+), 617 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 84b08bb377..6ac6a139d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -92,6 +92,7 @@ import com.vitorpamplona.quartz.experimental.profileGallery.dimension import com.vitorpamplona.quartz.experimental.profileGallery.fromEvent import com.vitorpamplona.quartz.experimental.profileGallery.hash import com.vitorpamplona.quartz.experimental.profileGallery.mimeType +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray @@ -1085,14 +1086,15 @@ class Account( fun consumeAndSendNip95( data: FileStorageEvent, signedEvent: FileStorageHeaderEvent, - relayList: List, ): Note? { if (!isWriteable()) return null - client.send(data, relayList = relayList.toSet()) + val relayList = computeRelayListToBroadcast(signedEvent) + + client.send(data, relayList = relayList) cache.justConsumeMyOwnEvent(data) - client.send(signedEvent, relayList = relayList.toSet()) + client.send(signedEvent, relayList = relayList) cache.justConsumeMyOwnEvent(signedEvent) return cache.getNoteIfExists(signedEvent.id) @@ -1161,7 +1163,6 @@ class Account( urlHeaderInfo: Map, caption: String?, contentWarningReason: String?, - relayList: Set, onReady: (Note) -> Unit, ) { val iMetas = @@ -1180,7 +1181,7 @@ class Account( ) } - signer.sign( + val template = PictureEvent.build(iMetas, caption ?: "") { caption?.let { hashtags(findHashtags(it)) @@ -1192,9 +1193,10 @@ class Account( // add geohashes // add title contentWarningReason?.let { contentWarning(contentWarningReason) } - }, - ) { - sendHeader(it, relayList = relayList, onReady) + } + + signAndComputeBroadcast(template) { event -> + cache.getNoteIfExists(event.id)?.let { onReady(it) } } } @@ -1205,7 +1207,6 @@ class Account( alt: String?, contentWarningReason: String?, originalHash: String? = null, - relayList: Set, onReady: (Note) -> Unit, ) { if (!isWriteable()) return @@ -1273,8 +1274,8 @@ class Account( } } - signer.sign(template) { - sendHeader(it, relayList = relayList, onReady) + signAndComputeBroadcast(template) { event -> + cache.getNoteIfExists(event.id)?.let { onReady(it) } } } @@ -1305,6 +1306,30 @@ class Account( } } + fun signAndComputeBroadcast( + template: EventTemplate, + broadcast: List = emptyList(), + onDone: (T) -> Unit = {}, + ) { + signer.sign(template) { event -> + cache.justConsumeMyOwnEvent(event) + val note = + if (event is AddressableEvent) { + cache.getOrCreateAddressableNote(event.address()) + } else { + cache.getOrCreateNote(event.id) + } + + val relayList = computeRelayListToBroadcast(note) + + client.send(event, relayList) + + broadcast.forEach { client.send(it, relayList) } + + onDone(event) + } + } + fun signAndSend( template: EventTemplate, relayList: Set, @@ -1581,7 +1606,7 @@ class Account( originalNote: Note, notify: HexKey?, summary: String? = null, - relayList: List, + broadcast: List, ) { if (!isWriteable()) return @@ -1593,9 +1618,14 @@ class Account( notify = notify, summary = summary, signer = signer, - ) { - cache.justConsumeMyOwnEvent(it) - client.send(it, relayList = relayList.toSet()) + ) { event -> + cache.justConsumeMyOwnEvent(event) + val note = cache.getOrCreateNote(event.id) + val relayList = computeRelayListToBroadcast(note) + + client.send(event, relayList = relayList) + + broadcast.forEach { client.send(it, relayList) } } } @@ -1715,7 +1745,7 @@ class Account( )?.relays()?.ifEmpty { null }?.toSet() if (relayList != null) { - client.send(signedEvent = wrap, relayList = relayList) + client.send(event = wrap, relayList = relayList) } else { val taggedUserInboxRelays = wrap.taggedUserIds().flatMapTo(mutableSetOf()) { pubkey -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 557c8a8785..6a7781ecea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.foundation.border import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -96,7 +95,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange @@ -108,7 +106,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.amethyst.ui.theme.subtleBorder -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -128,12 +125,6 @@ fun EditPostView( val scrollState = rememberScrollState() val scope = rememberCoroutineScope() - var showRelaysDialog by remember { mutableStateOf(false) } - var relayList = - remember { - accountViewModel.account.outboxRelays.flow.value - .toImmutableList() - } LaunchedEffect(Unit) { postViewModel.load(edit, versionLookingAt, accountViewModel) @@ -148,16 +139,6 @@ fun EditPostView( decorFitsSystemWindows = false, ), ) { - if (showRelaysDialog) { - RelaySelectionDialog( - preSelectedList = relayList, - onClose = { showRelaysDialog = false }, - onPost = { relayList = it }, - accountViewModel = accountViewModel, - nav = nav, - ) - } - Scaffold( topBar = { TopAppBar( @@ -169,22 +150,9 @@ fun EditPostView( ) { Spacer(modifier = StdHorzSpacer) - Box { - IconButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showRelaysDialog = true }, - ) { - Icon( - painter = painterRes(R.drawable.relays, 2), - contentDescription = stringRes(id = R.string.relay_list_selector), - modifier = Modifier.height(25.dp), - tint = MaterialTheme.colorScheme.onBackground, - ) - } - } PostButton( onPost = { - postViewModel.sendPost(relayList = relayList) + postViewModel.sendPost() scope.launch { delay(100) onClose() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 2ede4d54b9..5d10cc634a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder import com.vitorpamplona.quartz.nip94FileMetadata.alt @@ -120,18 +120,21 @@ open class EditPostViewModel : ViewModel() { editedFromNote = edit } - fun sendPost(relayList: List) { - viewModelScope.launch(Dispatchers.IO) { innerSendPost(relayList) } + fun sendPost() { + viewModelScope.launch(Dispatchers.IO) { innerSendPost() } } - suspend fun innerSendPost(relayList: List) { + suspend fun innerSendPost() { if (accountViewModel == null) { cancel() return } + val extraNotesToBroadcast = mutableListOf() + nip95attachments.forEach { - account?.sendNip95(it.first, it.second, relayList.toSet()) + extraNotesToBroadcast.add(it.first) + extraNotesToBroadcast.add(it.second) } val notify = @@ -147,7 +150,7 @@ open class EditPostViewModel : ViewModel() { originalNote = editedFromNote!!, notify = notify, summary = subject.text.ifBlank { null }, - relayList = relayList, + extraNotesToBroadcast, ) cancel() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 320793b386..4e6e8a1cbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -39,7 +39,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.joinAll @@ -82,13 +81,11 @@ open class NewMediaModel : ViewModel() { fun upload( context: Context, - relayList: List, onSucess: () -> Unit, onError: (String, String) -> Unit, ) { viewModelScope.launch { val myAccount = account ?: return@launch - if (relayList.isEmpty()) return@launch val serverToUse = selectedServer ?: return@launch val myMultiOrchestrator = multiOrchestrator ?: return@launch @@ -141,7 +138,7 @@ open class NewMediaModel : ViewModel() { withTimeoutOrNull(30000) { suspendCancellableCoroutine { continuation -> account?.createNip95(it.bytes, headerInfo = it.fileHeader, caption, if (sensitiveContent) "" else null) { nip95 -> - account?.consumeAndSendNip95(nip95.first, nip95.second, relayList) + account?.consumeAndSendNip95(nip95.first, nip95.second) continuation.resume(true) } } @@ -156,13 +153,12 @@ open class NewMediaModel : ViewModel() { withTimeoutOrNull(30000) { suspendCancellableCoroutine { continuation -> account?.sendHeader( - it.url, - it.magnet, - it.fileHeader, - caption, - if (sensitiveContent) "" else null, - it.uploadedHash, - relayList.toSet(), + url = it.url, + magnetUri = it.magnet, + headerInfo = it.fileHeader, + alt = caption, + contentWarningReason = if (sensitiveContent) "" else null, + originalHash = it.uploadedHash, ) { continuation.resume(true) } @@ -178,10 +174,9 @@ open class NewMediaModel : ViewModel() { withTimeoutOrNull(30000) { suspendCancellableCoroutine { continuation -> account?.sendAllAsOnePictureEvent( - imageUrls, - caption, - if (sensitiveContent) "" else null, - relayList.toSet(), + urlHeaderInfo = imageUrls, + caption = caption, + contentWarningReason = if (sensitiveContent) "" else null, ) { continuation.resume(true) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index 9252a51dc9..f5a6a49e84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -35,8 +35,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold @@ -49,7 +47,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -70,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer @@ -100,13 +96,6 @@ fun NewMediaView( postViewModel.load(account, uris) } - var showRelaysDialog by remember { mutableStateOf(false) } - var relayList = - remember { - accountViewModel.account.outboxRelays.flow.value - .toImmutableList() - } - Dialog( onDismissRequest = { onClose() }, properties = @@ -128,23 +117,9 @@ fun NewMediaView( ) { Spacer(modifier = StdHorzSpacer) - Box { - IconButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showRelaysDialog = true }, - ) { - Icon( - painter = painterRes(R.drawable.relays, 3), - contentDescription = stringRes(id = R.string.relay_list_selector), - modifier = Modifier.height(25.dp), - tint = MaterialTheme.colorScheme.onBackground, - ) - } - } - PostButton( onPost = { - postViewModel.upload(context, relayList, onClose, accountViewModel.toastManager::toast) + postViewModel.upload(context, onClose, accountViewModel.toastManager::toast) postViewModel.selectedServer?.let { if (it.type != ServerType.NIP95) { account.settings.changeDefaultFileServer(it) @@ -173,16 +148,6 @@ fun NewMediaView( ) }, ) { pad -> - if (showRelaysDialog) { - RelaySelectionDialog( - preSelectedList = relayList, - onClose = { showRelaysDialog = false }, - onPost = { relayList = it }, - accountViewModel = accountViewModel, - nav = nav, - ) - } - Surface( modifier = Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt index 99393183fe..1f97a05994 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPollVoteValueRange.kt @@ -66,7 +66,7 @@ fun NewPollVoteValueRange(pollViewModel: ShortNotePostViewModel) { onValueChange = { pollViewModel.updateMinZapAmountForPoll(it) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.weight(1f), - colors = if (pollViewModel.isValidvalueMinimum.value) colorValid else colorInValid, + colors = if (pollViewModel.isValidValueMinimum.value) colorValid else colorInValid, label = { Text( text = stringRes(R.string.poll_zap_value_min), @@ -88,7 +88,7 @@ fun NewPollVoteValueRange(pollViewModel: ShortNotePostViewModel) { onValueChange = { pollViewModel.updateMaxZapAmountForPoll(it) }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), modifier = Modifier.weight(1f), - colors = if (pollViewModel.isValidvalueMaximum.value) colorValid else colorInValid, + colors = if (pollViewModel.isValidValueMaximum.value) colorValid else colorInValid, label = { Text( text = stringRes(R.string.poll_zap_value_max), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt deleted file mode 100644 index 0b737a14ea..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/RelaySelectionDialog.kt +++ /dev/null @@ -1,278 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.actions - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.service.Nip11Retriever -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import kotlin.collections.map - -data class RelayList( - val relay: NormalizedRelayUrl, - val isSelected: Boolean, -) - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun RelaySelectionDialog( - preSelectedList: ImmutableList, - onClose: () -> Unit, - onPost: (list: ImmutableList) -> Unit, - accountViewModel: AccountViewModel, - nav: INav, -) { - var relays by remember { - mutableStateOf( - accountViewModel.account.client.allAvailableRelays().map { - RelayList( - relay = it, - isSelected = preSelectedList.any { relayUrl -> it == relayUrl }, - ) - }, - ) - } - - val hasSelectedRelay by remember { derivedStateOf { relays.any { it.isSelected } } } - - var selected by remember { mutableStateOf(true) } - - Dialog( - onDismissRequest = { onClose() }, - properties = - DialogProperties( - usePlatformDefaultWidth = false, - dismissOnClickOutside = false, - decorFitsSystemWindows = false, - ), - ) { - SetDialogToEdgeToEdge() - - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar( - actions = { - SaveButton( - onPost = { - val selectedRelays = relays.filter { it.isSelected }.map { it.relay }.toImmutableList() - onPost(selectedRelays) - onClose() - }, - isActive = hasSelectedRelay, - ) - Spacer(modifier = StdHorzSpacer) - }, - title = {}, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { onClose() }, - ) - } - }, - ) - }, - ) { pad -> - Column( - modifier = - Modifier.padding(pad).padding(start = 10.dp, end = 10.dp, top = 10.dp), - ) { - RelaySwitch( - text = stringRes(R.string.select_deselect_all), - checked = selected, - onClick = { - selected = !selected - relays = relays.mapIndexed { _, item -> item.copy(isSelected = selected) } - }, - ) - - LazyColumn( - contentPadding = FeedPadding, - ) { - itemsIndexed( - relays, - key = { _, item -> item.relay.url }, - ) { index, item -> - RenderRelaySwitch( - item, - onClick = { - relays = - relays.mapIndexed { j, item -> - if (index == j) { - item.copy(isSelected = !item.isSelected) - } else { - item - } - } - }, - accountViewModel, - nav, - ) - } - } - } - } - } -} - -@Composable -fun RenderRelaySwitch( - item: RelayList, - onClick: () -> Unit, - accountViewModel: AccountViewModel, - nav: INav, -) { - val context = LocalContext.current - - val relayInfo by loadRelayInfo(item.relay, accountViewModel) - - var openRelayDialog by remember { mutableStateOf(false) } - - if (openRelayDialog) { - RelayInformationDialog( - onClose = { openRelayDialog = false }, - relayInfo = relayInfo, - relay = item.relay, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - RelaySwitch( - text = item.relay.displayUrl(), - checked = item.isSelected, - onClick = onClick, - onLongPress = { - openRelayDialog = true - accountViewModel.retrieveRelayDocument( - relay = item.relay, - onInfo = { }, - onError = { relay, errorCode, exceptionMessage -> - val msg = - when (errorCode) { - Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - } - - accountViewModel.toastManager.toast( - stringRes(context, R.string.unable_to_download_relay_document), - msg, - ) - }, - ) - }, - ) -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun RelaySwitch( - text: String, - checked: Boolean, - onClick: () -> Unit, - onLongPress: () -> Unit = {}, -) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = - Modifier.combinedClickable( - onClick = onClick, - onLongClick = onLongPress, - ), - ) { - Text( - modifier = Modifier.weight(1f), - text = text, - ) - Switch( - checked = checked, - onCheckedChange = { onClick() }, - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 1b049286bc..a28cb0839c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -64,7 +64,6 @@ import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.hasGeohashes @@ -99,13 +98,14 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size +import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlin.collections.plus +import kotlin.coroutines.resume @Stable open class CommentPostViewModel : @@ -131,7 +131,7 @@ open class CommentPostViewModel : var account: Account? = null var externalIdentity by mutableStateOf(null) - var replyingTo: Note? by mutableStateOf(null) + var replyingTo: Note? by mutableStateOf(null) val iMetaAttachments = IMetaAttachments() var nip95attachments by mutableStateOf>>( @@ -177,9 +177,6 @@ open class CommentPostViewModel : var wantsZapraiser by mutableStateOf(false) override val zapRaiserAmount = mutableStateOf(null) - var showRelaysDialog by mutableStateOf(false) - var relayList by mutableStateOf?>(null) - fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null @@ -270,15 +267,15 @@ open class CommentPostViewModel : canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null multiOrchestrator = null - val localfowardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" } + val localForwardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" } forwardZapTo.value = SplitBuilder() - localfowardZapTo.forEach { + localForwardZapTo.forEach { val user = LocalCache.getOrCreateUser(it[1]) val value = it.last().toFloatOrNull() ?: 0f forwardZapTo.value.addItem(user, value) } forwardZapToEditting.value = TextFieldValue("") - wantsForwardZapTo = localfowardZapTo.isNotEmpty() + wantsForwardZapTo = localForwardZapTo.isNotEmpty() wantsToMarkAsSensitive = draftEvent.isSensitive() @@ -311,21 +308,23 @@ open class CommentPostViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val relayList = relayList + val extraNotesToBroadcast = mutableListOf() - if (nip95attachments.isNotEmpty() && relayList != null && relayList.isNotEmpty()) { + if (nip95attachments.isNotEmpty()) { val usedImages = template.tags.taggedQuoteIds().toSet() nip95attachments.forEach { if (usedImages.contains(it.second.id)) { - account?.sendNip95(it.first, it.second, relayList.toSet()) + extraNotesToBroadcast.add(it.first) + extraNotesToBroadcast.add(it.second) } } } - accountViewModel?.account?.signAndSendPrivatelyOrBroadcast( - template, - relayList = { relayList }, - ) + tryAndWait { continuation -> + accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) { + continuation.resume(it) + } + } accountViewModel?.deleteDraft(draftTag.current) @@ -378,7 +377,7 @@ open class CommentPostViewModel : msg = tagger.message, replyingTo = eventHint, ) { - tagger.pTags?.let { notify(it.map { it.toPTag() }) } + tagger.pTags?.let { pTagList -> notify(pTagList.map { it.toPTag() }) } hashtags(findHashtags(tagger.message)) references(findURLs(tagger.message)) @@ -398,7 +397,7 @@ open class CommentPostViewModel : msg = tagger.message, extId = externalIdentity, ) { - tagger.pTags?.let { notify(it.map { it.toPTag() }) } + tagger.pTags?.let { pTagList -> notify(pTagList.map { it.toPTag() }) } hashtags(findHashtags(tagger.message)) references(findURLs(tagger.message)) @@ -458,9 +457,9 @@ open class CommentPostViewModel : ) if (results.allGood) { - results.successful.forEach { - if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, alt, contentWarningReason) { nip95 -> + results.successful.forEach { state -> + if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { + account?.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) { nip95 -> nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } @@ -469,20 +468,20 @@ open class CommentPostViewModel : urlPreviews.update(message) } } - } else if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(it.result.url) + IMetaTagBuilder(state.result.url) .apply { - hash(it.result.fileHeader.hash) - size(it.result.fileHeader.size) - it.result.fileHeader.mimeType + hash(state.result.fileHeader.hash) + size(state.result.fileHeader.size) + state.result.fileHeader.mimeType ?.let { mimeType(it) } - it.result.fileHeader.dim + state.result.fileHeader.dim ?.let { dims(it) } - it.result.fileHeader.blurHash + state.result.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - it.result.magnet?.let { magnet(it) } - it.result.uploadedHash?.let { originalHash(it) } + state.result.magnet?.let { magnet(it) } + state.result.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } contentWarningReason?.let { sensitiveContent(contentWarningReason) } @@ -490,7 +489,7 @@ open class CommentPostViewModel : iMetaAttachments.replace(iMeta.url, iMeta) - message = message.insertUrlAtCursor(it.result.url) + message = message.insertUrlAtCursor(state.result.url) urlPreviews.update(message) } } @@ -546,21 +545,9 @@ open class CommentPostViewModel : emojiSuggestions?.reset() - showRelaysDialog = false - - reloadRelaySet() - draftTag.rotate() } - fun reloadRelaySet() { - val account = accountViewModel?.account ?: return - - relayList = - account.outboxRelays.flow.value - .toImmutableList() - } - fun deleteMediaToUpload(selected: SelectedMediaProcessing) { this.multiOrchestrator?.remove(selected) } @@ -569,8 +556,8 @@ open class CommentPostViewModel : notifying = notifying?.filter { it != userToRemove } } - override fun updateMessage(it: TextFieldValue) { - message = it + override fun updateMessage(newMessage: TextFieldValue) { + message = newMessage urlPreviews.update(message) if (message.selection.collapsed) { @@ -627,13 +614,13 @@ open class CommentPostViewModel : val wordToInsert = item.link.url + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare( - item.link.url, - { - Amethyst.Companion.instance.okHttpClients - .getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) - }, - ) + iMetaAttachments.downloadAndPrepare(item.link.url) { + Amethyst.Companion.instance.okHttpClients + .getHttpClient( + accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) + ?: false, + ) + } } message = message.replaceCurrentWord(wordToInsert) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 1fbdc21c77..f91fb778cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments import android.net.Uri import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -37,8 +36,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -46,7 +43,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -56,7 +52,6 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -85,7 +80,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp @@ -116,7 +110,6 @@ fun ReplyCommentPostScreen( val context = LocalContext.current LaunchedEffect(Unit) { - postViewModel.reloadRelaySet() reply?.let { postViewModel.reply(it) } @@ -159,20 +152,6 @@ fun GenericCommentPostScreen( verticalAlignment = CenterVertically, ) { Spacer(modifier = StdHorzSpacer) - - Box { - IconButton( - modifier = Modifier.align(Alignment.Center), - onClick = { postViewModel.showRelaysDialog = true }, - ) { - Icon( - painter = painterRes(R.drawable.relays, 5), - contentDescription = stringRes(id = R.string.relay_list_selector), - modifier = Modifier.height(25.dp), - tint = MaterialTheme.colorScheme.onBackground, - ) - } - } PostButton( onPost = { // uses the accountViewModel scope to avoid cancelling this @@ -209,15 +188,6 @@ fun GenericCommentPostScreen( ) }, ) { pad -> - if (postViewModel.showRelaysDialog) { - RelaySelectionDialog( - preSelectedList = postViewModel.relayList ?: persistentListOf(), - onClose = { postViewModel.showRelaysDialog = false }, - onPost = { postViewModel.relayList = it }, - accountViewModel = accountViewModel, - nav = nav, - ) - } Surface( modifier = Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 580f31de06..94cfbce292 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds import android.net.Uri import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -37,8 +36,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -47,10 +44,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -60,7 +54,6 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -88,7 +81,6 @@ import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton -import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp @@ -151,8 +143,6 @@ fun NewProductScreen( ) { WatchAndLoadMyEmojiList(accountViewModel) - var showRelaysDialog by remember { mutableStateOf(false) } - Scaffold( topBar = { TopAppBar( @@ -164,19 +154,6 @@ fun NewProductScreen( ) { Spacer(modifier = StdHorzSpacer) - Box { - IconButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showRelaysDialog = true }, - ) { - Icon( - painter = painterRes(R.drawable.relays, 1), - contentDescription = stringRes(id = R.string.relay_list_selector), - modifier = Modifier.height(25.dp), - tint = MaterialTheme.colorScheme.onBackground, - ) - } - } PostButton( onPost = { accountViewModel.viewModelScope.launch(Dispatchers.IO) { @@ -210,15 +187,6 @@ fun NewProductScreen( ) }, ) { pad -> - if (showRelaysDialog) { - RelaySelectionDialog( - preSelectedList = postViewModel.relayList ?: persistentListOf(), - onClose = { showRelaysDialog = false }, - onPost = { postViewModel.relayList = it }, - accountViewModel = accountViewModel, - nav = nav, - ) - } Surface( modifier = Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt index 9d56277f29..368e021d60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt @@ -54,7 +54,6 @@ fun GeoHashPostScreen( val context = LocalContext.current LaunchedEffect(Unit) { - postViewModel.reloadRelaySet() geohash?.let { postViewModel.newPostFor(GeohashId(it)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt index fd0934ac37..d96a6a847b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt @@ -54,7 +54,6 @@ fun HashtagPostScreen( val context = LocalContext.current LaunchedEffect(Unit) { - postViewModel.reloadRelaySet() hashtag?.let { postViewModel.newPostFor(HashtagId(it)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 1e22671f59..c3203f6dd9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -25,7 +25,6 @@ import android.net.Uri import android.os.Parcelable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -50,8 +49,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -62,7 +59,6 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.actions.RelaySelectionDialog import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -173,8 +169,6 @@ private fun NewPostScreenInner( accountViewModel: AccountViewModel, nav: Nav, ) { - val scope = rememberCoroutineScope() - WatchAndLoadMyEmojiList(accountViewModel) Scaffold( @@ -188,19 +182,6 @@ private fun NewPostScreenInner( ) { Spacer(modifier = StdHorzSpacer) - Box { - IconButton( - modifier = Modifier.align(Alignment.Center), - onClick = { postViewModel.showRelaysDialog = true }, - ) { - Icon( - painter = painterRes(R.drawable.relays, 6), - contentDescription = stringRes(id = R.string.relay_list_selector), - modifier = Modifier.height(25.dp), - tint = MaterialTheme.colorScheme.onBackground, - ) - } - } PostButton( onPost = { // uses the accountViewModel scope to avoid cancelling this @@ -238,15 +219,6 @@ private fun NewPostScreenInner( ) }, ) { pad -> - if (postViewModel.showRelaysDialog) { - RelaySelectionDialog( - preSelectedList = postViewModel.relayList ?: persistentListOf(), - onClose = { postViewModel.showRelaysDialog = false }, - onPost = { postViewModel.relayList = it }, - accountViewModel = accountViewModel, - nav = nav, - ) - } Surface( modifier = Modifier @@ -419,7 +391,7 @@ private fun NewPostScreenBody( } } - if (postViewModel.wantsZapraiser && postViewModel.hasLnAddress()) { + if (postViewModel.wantsZapRaiser && postViewModel.hasLnAddress()) { Row( verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), @@ -495,8 +467,8 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) { } if (postViewModel.canAddZapRaiser) { - AddZapraiserButton(postViewModel.wantsZapraiser) { - postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser + AddZapraiserButton(postViewModel.wantsZapRaiser) { + postViewModel.wantsZapRaiser = !postViewModel.wantsZapRaiser } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 90d5280f4c..af371a87cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -74,7 +74,6 @@ import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash @@ -115,12 +114,13 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size +import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import kotlin.coroutines.resume enum class UserSuggestionAnchor { MAIN_MESSAGE, @@ -151,8 +151,8 @@ open class ShortNotePostViewModel : var accountViewModel: AccountViewModel? = null var account: Account? = null - var originalNote: Note? by mutableStateOf(null) - var forkedFromNote: Note? by mutableStateOf(null) + var originalNote: Note? by mutableStateOf(null) + var forkedFromNote: Note? by mutableStateOf(null) var pTags by mutableStateOf?>(null) var eTags by mutableStateOf?>(null) @@ -185,9 +185,8 @@ open class ShortNotePostViewModel : var consensusThreshold: Int? = null var closedAt: Long? = null - var isValidRecipients = mutableStateOf(true) - var isValidvalueMaximum = mutableStateOf(true) - var isValidvalueMinimum = mutableStateOf(true) + var isValidValueMaximum = mutableStateOf(true) + var isValidValueMinimum = mutableStateOf(true) var isValidConsensusThreshold = mutableStateOf(true) var isValidClosedAt = mutableStateOf(true) @@ -212,12 +211,9 @@ open class ShortNotePostViewModel : // ZapRaiser var canAddZapRaiser by mutableStateOf(false) - var wantsZapraiser by mutableStateOf(false) + var wantsZapRaiser by mutableStateOf(false) override val zapRaiserAmount = mutableStateOf(null) - var showRelaysDialog by mutableStateOf(false) - var relayList by mutableStateOf?>(null) - fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null @@ -295,10 +291,10 @@ open class ShortNotePostViewModel : canUsePoll = originalNote == null multiOrchestrator = null - quote?.let { - message = TextFieldValue(message.text + "\nnostr:${it.toNEvent()}") + quote?.let { quotedNote -> + message = TextFieldValue(message.text + "\nnostr:${quotedNote.toNEvent()}") - it.author?.let { quotedUser -> + quotedNote.author?.let { quotedUser -> if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { if (forwardZapTo.value.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) { forwardZapTo.value.addItem(quotedUser) @@ -313,21 +309,21 @@ open class ShortNotePostViewModel : } } - fork?.let { - message = TextFieldValue(version?.event?.content ?: it.event?.content ?: "") + fork?.let { forkedNoted -> + message = TextFieldValue(version?.event?.content ?: forkedNoted.event?.content ?: "") - it.event?.isSensitiveOrNSFW()?.let { + forkedNoted.event?.isSensitiveOrNSFW()?.let { if (it) wantsToMarkAsSensitive = true } - it.event?.zapraiserAmount()?.let { + forkedNoted.event?.zapraiserAmount()?.let { zapRaiserAmount.value = it } - it.event?.zapSplitSetup()?.let { - val totalWeight = it.sumOf { if (it is ZapSplitSetupLnAddress) 0.0 else it.weight } + forkedNoted.event?.zapSplitSetup()?.let { setup -> + val totalWeight = setup.sumOf { if (it is ZapSplitSetupLnAddress) 0.0 else it.weight } - it.forEach { + setup.forEach { if (it is ZapSplitSetup) { forwardZapTo.value.addItem(LocalCache.getOrCreateUser(it.pubKeyHex), (it.weight / totalWeight).toFloat()) } @@ -336,7 +332,7 @@ open class ShortNotePostViewModel : // Only adds if it is not already set up. if (forwardZapTo.value.items.isEmpty()) { - it.author?.let { forkedAuthor -> + forkedNoted.author?.let { forkedAuthor -> if (forkedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { if (forwardZapTo.value.items.none { it.key.pubkeyHex == forkedAuthor.pubkeyHex }) forwardZapTo.value.addItem(forkedAuthor) if (forwardZapTo.value.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) forwardZapTo.value.addItem(accountViewModel.userProfile()) @@ -347,7 +343,7 @@ open class ShortNotePostViewModel : } } - it.author?.let { + forkedNoted.author?.let { if (this.pTags == null) { this.pTags = listOf(it) } else if (this.pTags?.contains(it) != true) { @@ -355,7 +351,7 @@ open class ShortNotePostViewModel : } } - forkedFromNote = it + forkedFromNote = forkedNoted } ?: run { forkedFromNote = null } @@ -380,15 +376,15 @@ open class ShortNotePostViewModel : canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null multiOrchestrator = null - val localfowardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" } + val localForwardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" } forwardZapTo.value = SplitBuilder() - localfowardZapTo.forEach { + localForwardZapTo.forEach { val user = LocalCache.getOrCreateUser(it[1]) val value = it.last().toFloatOrNull() ?: 0f forwardZapTo.value.addItem(user, value) } forwardZapToEditting.value = TextFieldValue("") - wantsForwardZapTo = localfowardZapTo.isNotEmpty() + wantsForwardZapTo = localForwardZapTo.isNotEmpty() wantsToMarkAsSensitive = draftEvent.isSensitive() @@ -398,11 +394,11 @@ open class ShortNotePostViewModel : wantsExclusiveGeoPost = draftEvent.kind == CommentEvent.KIND } - val zapraiser = draftEvent.zapraiserAmount() - wantsZapraiser = zapraiser != null + val zapRaiser = draftEvent.zapraiserAmount() + wantsZapRaiser = zapRaiser != null zapRaiserAmount.value = null - if (zapraiser != null) { - zapRaiserAmount.value = zapraiser + if (zapRaiser != null) { + zapRaiserAmount.value = zapRaiser } eTags = @@ -416,7 +412,7 @@ open class ShortNotePostViewModel : LocalCache.getOrCreateUser(it[1]) } - draftEvent.tags.filter { it.size > 3 && (it[0] == "e" || it[0] == "a") && it.get(3) == "fork" }.forEach { + draftEvent.tags.filter { it.size > 3 && (it[0] == "e" || it[0] == "a") && it[3] == "fork" }.forEach { val note = LocalCache.checkGetOrCreateNote(it[1]) forkedFromNote = note } @@ -470,21 +466,23 @@ open class ShortNotePostViewModel : suspend fun sendPostSync() { val template = createTemplate() ?: return - val relayList = relayList + val extraNotesToBroadcast = mutableListOf() - if (nip95attachments.isNotEmpty() && relayList != null && relayList.isNotEmpty()) { + if (nip95attachments.isNotEmpty()) { val usedImages = template.tags.taggedQuoteIds().toSet() nip95attachments.forEach { if (usedImages.contains(it.second.id)) { - account?.sendNip95(it.first, it.second, relayList.toSet()) + extraNotesToBroadcast.add(it.first) + extraNotesToBroadcast.add(it.second) } } } - accountViewModel?.account?.signAndSendPrivatelyOrBroadcast( - template, - relayList = { relayList }, - ) + tryAndWait { continuation -> + accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) { + continuation.resume(it) + } + } accountViewModel?.deleteDraft(draftTag.current) @@ -526,7 +524,7 @@ open class ShortNotePostViewModel : val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() - val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null + val localZapRaiserAmount = if (wantsZapRaiser) zapRaiserAmount.value else null val emojis = findEmoji(tagger.message, account?.emoji?.myEmojis?.value) val urls = findURLs(tagger.message) @@ -565,7 +563,7 @@ open class ShortNotePostViewModel : replyingTo = originalNote?.toEventHint(), forkingFrom = forkedFromNote?.toEventHint(), ) { - tagger.pTags?.let { notify(it.map { it.toPTag() }) } + tagger.pTags?.let { pTagList -> notify(pTagList.map { it.toPTag() }) } hashtags(findHashtags(tagger.message)) references(findURLs(tagger.message)) @@ -619,9 +617,9 @@ open class ShortNotePostViewModel : ) if (results.allGood) { - results.successful.forEach { - if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, alt, contentWarningReason) { nip95 -> + results.successful.forEach { state -> + if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { + account?.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) { nip95 -> nip95attachments = nip95attachments + nip95 val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } @@ -630,20 +628,20 @@ open class ShortNotePostViewModel : urlPreviews.update(message) } } - } else if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = - IMetaTagBuilder(it.result.url) + IMetaTagBuilder(state.result.url) .apply { - hash(it.result.fileHeader.hash) - size(it.result.fileHeader.size) - it.result.fileHeader.mimeType + hash(state.result.fileHeader.hash) + size(state.result.fileHeader.size) + state.result.fileHeader.mimeType ?.let { mimeType(it) } - it.result.fileHeader.dim + state.result.fileHeader.dim ?.let { dims(it) } - it.result.fileHeader.blurHash + state.result.fileHeader.blurHash ?.let { blurhash(it.blurhash) } - it.result.magnet?.let { magnet(it) } - it.result.uploadedHash?.let { originalHash(it) } + state.result.magnet?.let { magnet(it) } + state.result.uploadedHash?.let { originalHash(it) } alt?.let { alt(it) } contentWarningReason?.let { sensitiveContent(contentWarningReason) } @@ -651,7 +649,7 @@ open class ShortNotePostViewModel : iMetaAttachments.replace(iMeta.url, iMeta) - message = message.insertUrlAtCursor(it.result.url) + message = message.insertUrlAtCursor(state.result.url) urlPreviews.update(message) } } @@ -677,7 +675,7 @@ open class ShortNotePostViewModel : pTags = null wantsPoll = false - zapRecipients = mutableStateListOf() + zapRecipients = mutableStateListOf() pollOptions = newStateMapPollOptions() valueMaximum = null valueMinimum = null @@ -685,7 +683,7 @@ open class ShortNotePostViewModel : closedAt = null wantsInvoice = false - wantsZapraiser = false + wantsZapRaiser = false zapRaiserAmount.value = null wantsForwardZapTo = false @@ -706,21 +704,9 @@ open class ShortNotePostViewModel : emojiSuggestions?.reset() - showRelaysDialog = false - - reloadRelaySet() - draftTag.rotate() } - fun reloadRelaySet() { - val account = accountViewModel?.account ?: return - - relayList = - account.outboxRelays.flow.value - .toImmutableList() - } - fun deleteMediaToUpload(selected: SelectedMediaProcessing) { this.multiOrchestrator?.remove(selected) } @@ -733,8 +719,8 @@ open class ShortNotePostViewModel : updateMessage(TextFieldValue(message.text + " " + it)) } - override fun updateMessage(it: TextFieldValue) { - message = it + override fun updateMessage(newMessage: TextFieldValue) { + message = newMessage urlPreviews.update(message) if (message.selection.collapsed) { @@ -793,8 +779,12 @@ open class ShortNotePostViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare( item.link.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, - ) + ) { + Amethyst.instance.okHttpClients.getHttpClient( + accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) + ?: false, + ) + } } message = message.replaceCurrentWord(wordToInsert) @@ -811,13 +801,13 @@ open class ShortNotePostViewModel : message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && - (!wantsZapraiser || zapRaiserAmount.value != null) && + (!wantsZapRaiser || zapRaiserAmount.value != null) && ( !wantsPoll || ( pollOptions.values.all { it.isNotEmpty() } && - isValidvalueMinimum.value && - isValidvalueMaximum.value + isValidValueMinimum.value && + isValidValueMaximum.value ) ) && multiOrchestrator == null @@ -857,11 +847,11 @@ open class ShortNotePostViewModel : fun checkMinMax() { if ((valueMinimum ?: 0) > (valueMaximum ?: Long.MAX_VALUE)) { - isValidvalueMinimum.value = false - isValidvalueMaximum.value = false + isValidValueMinimum.value = false + isValidValueMaximum.value = false } else { - isValidvalueMinimum.value = true - isValidvalueMaximum.value = true + isValidValueMinimum.value = true + isValidValueMaximum.value = true } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index a0641bd826..144c5273c3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -237,18 +237,18 @@ class NostrClient( } fun sendIfExists( - signedEvent: Event, + event: Event, connectedRelay: NormalizedRelayUrl, ) { - relayPool.getRelay(connectedRelay)?.send(signedEvent) + relayPool.getRelay(connectedRelay)?.send(event) } fun send( - signedEvent: Event, + event: Event, relayList: Set, ) { - eventOutbox.markAsSending(signedEvent, relayList) - relayPool.send(signedEvent, relayList) + eventOutbox.markAsSending(event, relayList) + relayPool.send(event, relayList) } fun close(subscriptionId: String) { From 994363602d4ab10385b85a5914d21c56116f04b3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 12:00:17 -0400 Subject: [PATCH 046/185] Turns the relay information dialog into a route --- .../amethyst/ui/navigation/AppNavigation.kt | 2 + .../amethyst/ui/navigation/Routes.kt | 6 + .../amethyst/ui/note/RelayListRow.kt | 43 +- .../feed/types/RenderCreateChannelNote.kt | 45 +- .../loggedIn/relays/RelayInformationDialog.kt | 439 ----------------- .../loggedIn/relays/RelayInformationScreen.kt | 450 ++++++++++++++++++ .../common/BasicRelaySetupInfoDialog.kt | 73 +-- amethyst/src/main/res/values/strings.xml | 1 + 8 files changed, 466 insertions(+), 593 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationDialog.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 181bcfd7ca..da00bc896a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -80,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScree import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.redirect.LoadRedirectScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SecurityFiltersScreen @@ -135,6 +136,7 @@ fun AppNavigation( composableFromEndArgs { ThreadScreen(it.id, accountViewModel, nav) } composableFromEndArgs { HashtagScreen(it, accountViewModel, nav) } composableFromEndArgs { GeoHashScreen(it, accountViewModel, nav) } + composableFromEndArgs { RelayInformationScreen(it.url, accountViewModel, nav) } composableFromEndArgs { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) } composableFromEndArgs { ChatroomScreen(it.id.toString(), it.message, it.replyId, it.draftId, accountViewModel, nav) } composableFromEndArgs { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 14caf81b94..cd27911eff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -89,6 +89,10 @@ sealed class Route { val id: String, ) : Route() + @Serializable data class RelayInfo( + val url: String, + ) : Route() + @Serializable data class EphemeralChat( val id: String, val relayUrl: String, @@ -197,6 +201,8 @@ fun getRouteWithArguments(navController: NavHostController): Route? { dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() + dest.hasRoute() -> entry.toRoute() + dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 024faaaf74..110aacf9f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -53,13 +53,12 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter @@ -130,18 +129,6 @@ fun RenderRelay( ) { val relayInfo by loadRelayInfo(relay, accountViewModel) - var openRelayDialog by remember { mutableStateOf(false) } - - if (openRelayDialog) { - RelayInformationDialog( - onClose = { openRelayDialog = false }, - relayInfo = relayInfo, - relay = relay, - accountViewModel = accountViewModel, - nav = nav, - ) - } - val clipboardManager = LocalClipboardManager.current val clickableModifier = remember(relay) { @@ -153,33 +140,7 @@ fun RenderRelay( onLongClick = { clipboardManager.setText(AnnotatedString(relay.url)) }, - onClick = { - openRelayDialog = true - accountViewModel.retrieveRelayDocument( - relay, - onInfo = { }, - onError = { url, errorCode, exceptionMessage -> - accountViewModel.toastManager.toast( - R.string.unable_to_download_relay_document, - when (errorCode) { - Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> - R.string.relay_information_document_error_failed_to_assemble_url - - Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> - R.string.relay_information_document_error_failed_to_reach_server - - Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> - R.string.relay_information_document_error_failed_to_parse_response - - Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> - R.string.relay_information_document_error_failed_with_http - }, - url.url, - exceptionMessage ?: errorCode.toString(), - ) - }, - ) - }, + onClick = { nav.nav(Route.RelayInfo(relay.url)) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt index 2890217475..fbfcdcbcfc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -52,16 +52,15 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Constants import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter import com.vitorpamplona.amethyst.ui.theme.Size20dp @@ -241,18 +240,6 @@ fun RenderRelayLinePublicChat( @Suppress("ProduceStateDoesNotAssignValue") val relayInfo by loadRelayInfo(relay, accountViewModel) - var openRelayDialog by remember { mutableStateOf(false) } - - if (openRelayDialog) { - RelayInformationDialog( - onClose = { openRelayDialog = false }, - relayInfo = relayInfo, - relay = relay, - accountViewModel = accountViewModel, - nav = nav, - ) - } - val clipboardManager = LocalClipboardManager.current val clickableModifier = remember(relay) { @@ -260,39 +247,13 @@ fun RenderRelayLinePublicChat( onLongClick = { clipboardManager.setText(AnnotatedString(relay.url)) }, - onClick = { - openRelayDialog = true - accountViewModel.retrieveRelayDocument( - relay = relay, - onInfo = {}, - onError = { relay, errorCode, exceptionMessage -> - accountViewModel.toastManager.toast( - R.string.unable_to_download_relay_document, - when (errorCode) { - Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> - R.string.relay_information_document_error_failed_to_assemble_url - - Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> - R.string.relay_information_document_error_failed_to_reach_server - - Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> - R.string.relay_information_document_error_failed_to_parse_response - - Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> - R.string.relay_information_document_error_failed_with_http - }, - relay.url, - exceptionMessage ?: errorCode.toString(), - ) - }, - ) - }, + onClick = { nav.nav(Route.RelayInfo(relay.url)) }, ) } RenderRelayLine( relay.displayUrl(), - relayInfo?.icon, + relayInfo.icon, clickableModifier, showPicture = accountViewModel.settings.showProfilePictures.value, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationDialog.kt deleted file mode 100644 index 84b450de91..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationDialog.kt +++ /dev/null @@ -1,439 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.components.ClickableEmail -import com.vitorpamplona.amethyst.ui.components.ClickableUrl -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav -import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon -import com.vitorpamplona.amethyst.ui.note.UserCompose -import com.vitorpamplona.amethyst.ui.note.timeAgo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer -import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier -import com.vitorpamplona.amethyst.ui.theme.Size10dp -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.StdPadding -import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayDebugMessage -import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl -import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList -import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation -import kotlinx.collections.immutable.toImmutableList - -@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) -@Composable -fun RelayInformationDialog( - onClose: () -> Unit, - relay: NormalizedRelayUrl, - relayInfo: Nip11RelayInformation, - accountViewModel: AccountViewModel, - nav: INav, -) { - val newNav = rememberExtendedNav(nav, onClose) - - val messages = - remember(relay) { - RelayStats - .get(url = relay) - .messages - .snapshot() - .values - .sortedByDescending { it.time } - .toImmutableList() - } - - Dialog( - onDismissRequest = { onClose() }, - properties = - DialogProperties( - usePlatformDefaultWidth = false, - dismissOnClickOutside = false, - ), - ) { - SetDialogToEdgeToEdge() - - Scaffold( - modifier = Modifier.fillMaxSize(), - topBar = { - TopAppBar( - actions = {}, - title = { - Text(relay.displayUrl()) - }, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - BackButton( - onPress = { onClose() }, - ) - } - }, - ) - }, - ) { pad -> - LazyColumn( - modifier = - Modifier - .padding(pad) - .consumeWindowInsets(pad) - .padding(bottom = Size10dp, start = Size10dp, end = Size10dp) - .fillMaxSize(), - ) { - item { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = StdPadding.fillMaxWidth(), - ) { - Column { - RenderRelayIcon( - displayUrl = relay.displayUrl(), - iconUrl = relayInfo.icon, - loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, - RelayStats.get(relay).pingInMs, - iconModifier = LargeRelayIconModifier, - ) - } - - Spacer(modifier = DoubleHorzSpacer) - - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Title(relayInfo.name?.trim() ?: "") - Spacer(modifier = HalfVertSpacer) - SubtitleContent(relay.url) - } - } - } - item { - Section(stringRes(R.string.description)) - - SectionContent(relayInfo.description?.trim() ?: "") - } - item { - Section(stringRes(R.string.owner)) - - relayInfo.pubkey?.let { - DisplayOwnerInformation(it, accountViewModel, newNav) - } - } - item { - Section(stringRes(R.string.contact)) - - Box(modifier = Modifier.padding(start = 10.dp)) { - relayInfo.contact?.let { - if (it.startsWith("https:")) { - ClickableUrl(urlText = it, url = it) - } else if (it.startsWith("mailto:") || it.contains('@')) { - ClickableEmail(it) - } else { - SectionContent(it) - } - } - } - } - item { - Section(stringRes(R.string.software)) - - DisplaySoftwareInformation(relayInfo) - - Section(stringRes(R.string.version)) - - SectionContent(relayInfo.version ?: "") - } - item { - Section(stringRes(R.string.supports)) - - DisplaySupportedNips(relayInfo) - } - item { - relayInfo.fees?.admission?.let { - if (it.isNotEmpty()) { - Section(stringRes(R.string.admission_fees)) - - it.forEach { item -> SectionContent("${item.amount?.div(1000) ?: 0} sats") } - } - } - - relayInfo.payments_url?.let { - Section(stringRes(R.string.payments_url)) - - Box(modifier = Modifier.padding(start = 10.dp)) { - ClickableUrl( - urlText = it, - url = it, - ) - } - } - } - item { - relayInfo.limitation?.let { - Section(stringRes(R.string.limitations)) - val authRequiredText = - if (it.auth_required ?: false) stringRes(R.string.yes) else stringRes(R.string.no) - - val paymentRequiredText = - if (it.payment_required ?: false) stringRes(R.string.yes) else stringRes(R.string.no) - - val restrictedWritesText = - if (it.restricted_writes ?: false) stringRes(R.string.yes) else stringRes(R.string.no) - - Column { - SectionContent( - "${stringRes(R.string.message_length)}: ${it.max_message_length ?: 0}", - ) - SectionContent( - "${stringRes(R.string.subscriptions)}: ${it.max_subscriptions ?: 0}", - ) - SectionContent("${stringRes(R.string.filters)}: ${it.max_filters ?: 0}") - SectionContent( - "${stringRes(R.string.subscription_id_length)}: ${it.max_subid_length ?: 0}", - ) - SectionContent("${stringRes(R.string.minimum_prefix)}: ${it.min_prefix ?: 0}") - SectionContent( - "${stringRes(R.string.maximum_event_tags)}: ${it.max_event_tags ?: 0}", - ) - SectionContent( - "${stringRes(R.string.content_length)}: ${it.max_content_length ?: 0}", - ) - SectionContent( - "${stringRes(R.string.max_limit)}: ${it.max_limit ?: 0}", - ) - SectionContent("${stringRes(R.string.minimum_pow)}: ${it.min_pow_difficulty ?: 0}") - SectionContent("${stringRes(R.string.auth)}: $authRequiredText") - SectionContent("${stringRes(R.string.payment)}: $paymentRequiredText") - SectionContent("${stringRes(R.string.restricted_writes)}: $restrictedWritesText") - } - } - } - item { - relayInfo.relay_countries?.let { - Section(stringRes(R.string.countries)) - - FlowRow { it.forEach { item -> SectionContent(item) } } - } - } - item { - relayInfo.language_tags?.let { - Section(stringRes(R.string.languages)) - - FlowRow { it.forEach { item -> SectionContent(item) } } - } - } - item { - relayInfo.tags?.let { - Section(stringRes(R.string.tags)) - - FlowRow { it.forEach { item -> SectionContent(item) } } - } - } - item { - relayInfo.posting_policy?.let { - Section(stringRes(R.string.posting_policy)) - - Box(Modifier.padding(10.dp)) { - ClickableUrl( - it, - it, - ) - } - } - } - - item { - Section(stringRes(R.string.relay_error_messages)) - } - - items(messages) { msg -> - Row { - RenderDebugMessage(msg, accountViewModel, newNav) - } - - Spacer(modifier = StdVertSpacer) - } - } - } - } -} - -@Composable -private fun RenderDebugMessage( - msg: RelayDebugMessage, - accountViewModel: AccountViewModel, - newNav: INav, -) { - SelectionContainer { - val context = LocalContext.current - val color = - remember { - mutableStateOf(Color.Transparent) - } - TranslatableRichTextViewer( - content = - remember { - "${timeAgo(msg.time, context)}, ${msg.type.name}: ${msg.message}" - }, - canPreview = false, - quotesLeft = 0, - modifier = Modifier.fillMaxWidth(), - tags = EmptyTagList, - backgroundColor = color, - id = msg.hashCode().toString(), - accountViewModel = accountViewModel, - nav = newNav, - ) - } -} - -@Composable -@OptIn(ExperimentalLayoutApi::class) -private fun DisplaySupportedNips(relayInfo: Nip11RelayInformation) { - FlowRow { - relayInfo.supported_nips?.forEach { item -> - val text = item.toString().padStart(2, '0') - Box(Modifier.padding(10.dp)) { - ClickableUrl( - urlText = text, - url = "https://github.com/nostr-protocol/nips/blob/master/$text.md", - ) - } - } - - relayInfo.supported_nip_extensions?.forEach { item -> - val text = item.padStart(2, '0') - Box(Modifier.padding(10.dp)) { - ClickableUrl( - urlText = text, - url = "https://github.com/nostr-protocol/nips/blob/master/$text.md", - ) - } - } - } -} - -@Composable -private fun DisplaySoftwareInformation(relayInfo: Nip11RelayInformation) { - val url = (relayInfo.software ?: "").replace("git+", "") - Box(modifier = Modifier.padding(start = 10.dp)) { - ClickableUrl( - urlText = url, - url = url, - ) - } -} - -@Composable -private fun DisplayOwnerInformation( - userHex: String, - accountViewModel: AccountViewModel, - nav: INav, -) { - LoadUser(baseUserHex = userHex, accountViewModel) { - CrossfadeIfEnabled(it, accountViewModel = accountViewModel) { - if (it != null) { - UserCompose( - baseUser = it, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - } -} - -@Composable -fun Title(text: String) { - Text( - text = text, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - ) -} - -@Composable -fun SubtitleContent(text: String) { - Text( - text = text, - ) -} - -@Composable -fun Section(text: String) { - Spacer(modifier = DoubleVertSpacer) - Text( - text = text, - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - ) - Spacer(modifier = DoubleVertSpacer) -} - -@Composable -fun SectionContent(text: String) { - Text( - modifier = Modifier.padding(start = 10.dp), - text = text, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt new file mode 100644 index 0000000000..d5578ac6d1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -0,0 +1,450 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.FeatureSetType +import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.ui.components.ClickableEmail +import com.vitorpamplona.amethyst.ui.components.ClickableUrl +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon +import com.vitorpamplona.amethyst.ui.note.UserCompose +import com.vitorpamplona.amethyst.ui.note.timeAgo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer +import com.vitorpamplona.amethyst.ui.theme.LargeRelayIconModifier +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayDebugMessage +import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl +import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun RelayInformationScreen( + relayUrl: String, + accountViewModel: AccountViewModel, + nav: INav, +) { + RelayUrlNormalizer.normalizeOrNull(relayUrl)?.let { + RelayInformationScreen( + relay = it, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RelayInformationScreen( + relay: NormalizedRelayUrl, + accountViewModel: AccountViewModel, + nav: INav, +) { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + actions = {}, + title = { + Text(relay.displayUrl()) + }, + navigationIcon = { + Row { + Spacer(modifier = StdHorzSpacer) + BackButton( + onPress = nav::popBack, + ) + } + }, + ) + }, + ) { pad -> + val relayInfo by loadRelayInfo(relay, accountViewModel) + + val messages = + remember(relay) { + RelayStats + .get(url = relay) + .messages + .snapshot() + .values + .sortedByDescending { it.time } + .toImmutableList() + } + + LazyColumn( + modifier = + Modifier + .padding(pad) + .consumeWindowInsets(pad) + .padding(bottom = Size10dp, start = Size10dp, end = Size10dp) + .fillMaxSize(), + ) { + item { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + modifier = StdPadding.fillMaxWidth(), + ) { + Column { + RenderRelayIcon( + displayUrl = relay.displayUrl(), + iconUrl = relayInfo.icon, + loadProfilePicture = accountViewModel.settings.showProfilePictures.value, + loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + RelayStats.get(relay).pingInMs, + iconModifier = LargeRelayIconModifier, + ) + } + + Spacer(modifier = DoubleHorzSpacer) + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Title(relayInfo.name?.trim() ?: "") + Spacer(modifier = HalfVertSpacer) + SubtitleContent(relay.url) + } + } + } + item { + Section(stringRes(R.string.description)) + + SectionContent(relayInfo.description?.trim() ?: stringRes(R.string.no_description)) + } + relayInfo.pubkey?.let { + item { + Section(stringRes(R.string.owner)) + DisplayOwnerInformation(it, accountViewModel, nav) + } + } + relayInfo.contact?.let { + item { + Section(stringRes(R.string.contact)) + + Box(modifier = Modifier.padding(start = 10.dp)) { + if (it.startsWith("https:")) { + ClickableUrl(urlText = it, url = it) + } else if (it.startsWith("mailto:") || it.contains('@')) { + ClickableEmail(it) + } else { + SectionContent(it) + } + } + } + } + relayInfo.software?.let { + item { + Section(stringRes(R.string.software)) + + DisplaySoftwareInformation(it) + + Section(stringRes(R.string.version)) + + SectionContent(relayInfo.version ?: "") + } + } + relayInfo.supported_nips?.let { + if (it.isNotEmpty()) { + item { + Section(stringRes(R.string.supports)) + + DisplaySupportedNips(it, relayInfo.supported_nip_extensions) + } + } + } + relayInfo.fees?.admission?.let { + item { + if (it.isNotEmpty()) { + Section(stringRes(R.string.admission_fees)) + + it.forEach { item -> SectionContent("${item.amount?.div(1000) ?: 0} sats") } + } + } + } + + relayInfo.payments_url?.let { + item { + Section(stringRes(R.string.payments_url)) + + Box(modifier = Modifier.padding(start = 10.dp)) { + ClickableUrl( + urlText = it, + url = it, + ) + } + } + } + + relayInfo.limitation?.let { + item { + Section(stringRes(R.string.limitations)) + val authRequiredText = + if (it.auth_required ?: false) stringRes(R.string.yes) else stringRes(R.string.no) + + val paymentRequiredText = + if (it.payment_required ?: false) stringRes(R.string.yes) else stringRes(R.string.no) + + val restrictedWritesText = + if (it.restricted_writes ?: false) stringRes(R.string.yes) else stringRes(R.string.no) + + Column { + SectionContent( + "${stringRes(R.string.message_length)}: ${it.max_message_length ?: 0}", + ) + SectionContent( + "${stringRes(R.string.subscriptions)}: ${it.max_subscriptions ?: 0}", + ) + SectionContent("${stringRes(R.string.filters)}: ${it.max_filters ?: 0}") + SectionContent( + "${stringRes(R.string.subscription_id_length)}: ${it.max_subid_length ?: 0}", + ) + SectionContent("${stringRes(R.string.minimum_prefix)}: ${it.min_prefix ?: 0}") + SectionContent( + "${stringRes(R.string.maximum_event_tags)}: ${it.max_event_tags ?: 0}", + ) + SectionContent( + "${stringRes(R.string.content_length)}: ${it.max_content_length ?: 0}", + ) + SectionContent( + "${stringRes(R.string.max_limit)}: ${it.max_limit ?: 0}", + ) + SectionContent("${stringRes(R.string.minimum_pow)}: ${it.min_pow_difficulty ?: 0}") + SectionContent("${stringRes(R.string.auth)}: $authRequiredText") + SectionContent("${stringRes(R.string.payment)}: $paymentRequiredText") + SectionContent("${stringRes(R.string.restricted_writes)}: $restrictedWritesText") + } + } + } + relayInfo.relay_countries?.let { + item { + Section(stringRes(R.string.countries)) + + FlowRow { it.forEach { item -> SectionContent(item) } } + } + } + relayInfo.language_tags?.let { + item { + Section(stringRes(R.string.languages)) + + FlowRow { it.forEach { item -> SectionContent(item) } } + } + } + relayInfo.tags?.let { + item { + Section(stringRes(R.string.tags)) + + FlowRow { it.forEach { item -> SectionContent(item) } } + } + } + relayInfo.posting_policy?.let { + item { + Section(stringRes(R.string.posting_policy)) + + Box(Modifier.padding(10.dp)) { + ClickableUrl( + it, + it, + ) + } + } + } + + item { + Section(stringRes(R.string.relay_error_messages)) + } + + items(messages) { msg -> + Row { + RenderDebugMessage(msg, accountViewModel, nav) + } + + Spacer(modifier = StdVertSpacer) + } + } + } +} + +@Composable +private fun RenderDebugMessage( + msg: RelayDebugMessage, + accountViewModel: AccountViewModel, + newNav: INav, +) { + SelectionContainer { + val context = LocalContext.current + val color = + remember { + mutableStateOf(Color.Transparent) + } + TranslatableRichTextViewer( + content = + remember { + "${timeAgo(msg.time, context)}, ${msg.type.name}: ${msg.message}" + }, + canPreview = false, + quotesLeft = 0, + modifier = Modifier.fillMaxWidth(), + tags = EmptyTagList, + backgroundColor = color, + id = msg.hashCode().toString(), + accountViewModel = accountViewModel, + nav = newNav, + ) + } +} + +@Composable +@OptIn(ExperimentalLayoutApi::class) +private fun DisplaySupportedNips( + supportedNips: List, + supportedNipExtensions: List?, +) { + FlowRow { + supportedNips.forEach { item -> + val text = item.toString().padStart(2, '0') + Box(Modifier.padding(10.dp)) { + ClickableUrl( + urlText = text, + url = "https://github.com/nostr-protocol/nips/blob/master/$text.md", + ) + } + } + + supportedNipExtensions?.forEach { item -> + val text = item.padStart(2, '0') + Box(Modifier.padding(10.dp)) { + ClickableUrl( + urlText = text, + url = "https://github.com/nostr-protocol/nips/blob/master/$text.md", + ) + } + } + } +} + +@Composable +private fun DisplaySoftwareInformation(software: String) { + val url = software.replace("git+", "") + Box(modifier = Modifier.padding(start = 10.dp)) { + ClickableUrl( + urlText = url, + url = url, + ) + } +} + +@Composable +private fun DisplayOwnerInformation( + userHex: String, + accountViewModel: AccountViewModel, + nav: INav, +) { + LoadUser(baseUserHex = userHex, accountViewModel) { + CrossfadeIfEnabled(it, accountViewModel = accountViewModel) { + if (it != null) { + UserCompose( + baseUser = it, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + } +} + +@Composable +fun Title(text: String) { + Text( + text = text, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + ) +} + +@Composable +fun SubtitleContent(text: String) { + Text( + text = text, + ) +} + +@Composable +fun Section(text: String) { + Spacer(modifier = DoubleVertSpacer) + Text( + text = text, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + ) + Spacer(modifier = DoubleVertSpacer) +} + +@Composable +fun SectionContent(text: String) { + Text( + modifier = Modifier.padding(start = 10.dp), + text = text, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 64c7fb1255..8da93ea6c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -22,19 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.service.Nip11Retriever -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog -import com.vitorpamplona.amethyst.ui.stringRes @Composable fun BasicRelaySetupInfoDialog( @@ -43,75 +35,14 @@ fun BasicRelaySetupInfoDialog( accountViewModel: AccountViewModel, nav: INav, ) { - val context = LocalContext.current - val relayInfo by loadRelayInfo(item.relay, accountViewModel) - var openRelayDialog by remember { mutableStateOf(false) } - - if (openRelayDialog) { - RelayInformationDialog( - onClose = { openRelayDialog = false }, - relayInfo = relayInfo, - relay = item.relay, - accountViewModel = accountViewModel, - nav = nav, - ) - } - BasicRelaySetupInfoClickableRow( item = item, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, onDelete = onDelete, accountViewModel = accountViewModel, - onClick = { - openRelayDialog = true - accountViewModel.retrieveRelayDocument( - relay = item.relay, - onInfo = { }, - onError = { relay, errorCode, exceptionMessage -> - val msg = - when (errorCode) { - Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - - Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - - Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - - Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS -> - stringRes( - context, - R.string.relay_information_document_error_assemble_url, - relay.url, - exceptionMessage, - ) - } - - accountViewModel.toastManager.toast( - stringRes(context, R.string.unable_to_download_relay_document), - msg, - ) - }, - ) - }, + onClick = { nav.nav(Route.RelayInfo(item.relay.url)) }, ) } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index e7083237cc..82a56524ee 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -107,6 +107,7 @@ My Awesome Group Picture Url Description + Description not found "About us.. " What\'s on your mind? Write a message… From 0d174aae30331685d9cc760790017af216bf7f5c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 14:34:52 -0400 Subject: [PATCH 047/185] Refining the outbox relay selection with all hints. --- .../vitorpamplona/amethyst/model/Account.kt | 282 ++++++++---------- 1 file changed, 124 insertions(+), 158 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 6ac6a139d3..afac211c35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -97,7 +97,10 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.downloadFirstEvent import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient @@ -131,7 +134,6 @@ import com.vitorpamplona.quartz.nip10Notes.content.findURLs import com.vitorpamplona.quartz.nip17Dm.NIP17Factory import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes @@ -793,43 +795,27 @@ class Account( relaysItCameFrom } - fun computeRelayListToBroadcast(event: Event): Set { - val author = cache.getUserIfExists(event.pubKey) + private fun computeRelayListForLinkedUser(user: User): Set = + if (user == userProfile()) { + notificationRelays.flow.value + } else { + user.inboxRelays().ifEmpty { null }?.toSet() + ?: (cache.relayHints.hintsForKey(user.pubkeyHex).toSet() + user.relaysBeingUsed.keys) + } - val authorOutboxRelays = - if (author != null) { - if (author == userProfile()) { - outboxRelays.flow.value - } else { - author.outboxRelays().ifEmpty { null }?.toSet() - ?: cache.relayHints - .hintsForKey(author.pubkeyHex) - .ifEmpty { null } - ?.toSet() - ?: emptySet() - } - } else { - cache.relayHints - .hintsForKey(event.pubKey) - .ifEmpty { null } - ?.toSet() - emptySet() - } - - val taggedUserInboxRelays = - event.taggedUserIds().flatMapTo(mutableSetOf()) { pubkey -> - if (pubkey == userProfile().pubkeyHex) { - notificationRelays.flow.value - } else { - LocalCache - .getUserIfExists(pubkey) - ?.inboxRelays() - ?.ifEmpty { null } - ?.toSet() - ?: cache.relayHints.hintsForKey(pubkey).toSet() - } - } + private fun computeRelayListForLinkedUser(pubkey: HexKey): Set = + if (pubkey == userProfile().pubkeyHex) { + notificationRelays.flow.value + } else { + LocalCache + .getUserIfExists(pubkey) + ?.inboxRelays() + ?.ifEmpty { null } + ?.toSet() + ?: cache.relayHints.hintsForKey(pubkey).toSet() + } + private fun computeRelaysForChannels(event: Event): Set { val isInChannel = if ( event is ChannelMessageEvent || @@ -849,109 +835,118 @@ class Account( null } - val channelRelays = - if (isInChannel != null) { - val channel = LocalCache.checkGetOrCreateChannel(isInChannel) - channel?.relays() ?: emptySet() + return if (isInChannel != null) { + val channel = LocalCache.checkGetOrCreateChannel(isInChannel) + channel?.relays() ?: emptySet() + } else { + emptySet() + } + } + + fun computeRelayListToBroadcast(event: Event): Set { + if (event is GiftWrapEvent) { + val receiver = event.recipientPubKey() + if (receiver != null) { + val relayList = + cache + .getOrCreateUser(receiver) + .dmInboxRelayList() + ?.relays() + ?.ifEmpty { null } + if (relayList != null) { + client.send(event, relayList.toSet()) + } else { + val publicRelayList = computeRelayListForLinkedUser(receiver) + client.send(event, publicRelayList) + } } else { - emptySet() + return emptySet() } + } + if (event is WrappedEvent) { + return emptySet() + } - val replyRelays = - cache.computeReplyTo(event).flatMapTo(mutableSetOf()) { - val existingRelays = it.relays.toSet() - val replyToAuthor = it.author + val relayList = mutableSetOf() - val replyAuthorRelays = - if (replyToAuthor != null) { - if (replyToAuthor == userProfile()) { - outboxRelays.flow.value - } else { - replyToAuthor.outboxRelays().ifEmpty { null }?.toSet() - ?: cache.relayHints - .hintsForKey(replyToAuthor.pubkeyHex) - .ifEmpty { null } - ?.toSet() - ?: emptySet() - } + val author = cache.getUserIfExists(event.pubKey) + + if (author != null) { + if (author == userProfile()) { + relayList.addAll(outboxRelays.flow.value) + } else { + relayList.addAll( + author.outboxRelays().ifEmpty { null } + ?: cache.relayHints.hintsForKey(author.pubkeyHex), + ) + } + } else { + relayList.addAll(cache.relayHints.hintsForKey(event.pubKey)) + } + + if (event is PubKeyHintProvider) { + event.pubKeyHints().forEach { + relayList.add(it.relay) + } + event.linkedPubKeys().forEach { pubkey -> + relayList.addAll(computeRelayListForLinkedUser(pubkey)) + } + } + + if (event is EventHintProvider) { + event.eventHints().forEach { + relayList.add(it.relay) + } + event.linkedEventIds().forEach { eventId -> + cache.getNoteIfExists(eventId)?.let { linkedNote -> + val linkedNoteAuthor = linkedNote.author + + if (linkedNoteAuthor != null) { + relayList.addAll(computeRelayListForLinkedUser(linkedNoteAuthor)) } else { - emptySet() + relayList.addAll(linkedNote.relays.toSet()) } - existingRelays + replyAuthorRelays + linkedNote.event?.let { linkedEvent -> + relayList.addAll(computeRelaysForChannels(linkedEvent)) + } + } } + } - return authorOutboxRelays + taggedUserInboxRelays + channelRelays + replyRelays + if (event is AddressHintProvider) { + event.addressHints().forEach { + relayList.add(it.relay) + } + event.linkedAddressIds().forEach { addressId -> + cache.getAddressableNoteIfExists(addressId)?.let { linkedNote -> + val linkedNoteAuthor = linkedNote.author + + if (linkedNoteAuthor != null) { + relayList.addAll(computeRelayListForLinkedUser(linkedNoteAuthor)) + } else { + relayList.addAll(linkedNote.relays.toSet()) + } + + linkedNote.event?.let { linkedEvent -> + relayList.addAll(computeRelaysForChannels(linkedEvent)) + } + } + } + } + + relayList.addAll(computeRelaysForChannels(event)) + + return relayList } fun computeRelayListToBroadcast(note: Note): Set { - val author = note.author - - val authorOutboxRelays = - if (author != null) { - if (author == userProfile()) { - outboxRelays.flow.value - } else { - author.outboxRelays().ifEmpty { null }?.toSet() - ?: cache.relayHints - .hintsForKey(author.pubkeyHex) - .ifEmpty { null } - ?.toSet() - ?: emptySet() - } - } else { - emptySet() - } - - val taggedUserInboxRelays = - note.event?.taggedUserIds()?.flatMapTo(mutableSetOf()) { pubkey -> - if (pubkey == userProfile().pubkeyHex) { - notificationRelays.flow.value - } else { - LocalCache - .getUserIfExists(pubkey) - ?.inboxRelays() - ?.ifEmpty { null } - ?.toSet() - ?: cache.relayHints.hintsForKey(pubkey).toSet() - } - } ?: emptySet() - - val isInChannel = note.channelHex() - val channelRelays = - if (isInChannel != null) { - val channel = LocalCache.checkGetOrCreateChannel(isInChannel) - channel?.relays() ?: emptySet() - } else { - emptySet() - } - - val replyRelays = - note.replyTo?.flatMapTo(mutableSetOf()) { - val existingRelays = it.relays.toSet() - - val replyToAuthor = it.author - - val replyAuthorRelays = - if (replyToAuthor != null) { - if (replyToAuthor == userProfile()) { - outboxRelays.flow.value - } else { - replyToAuthor.outboxRelays().ifEmpty { null }?.toSet() - ?: cache.relayHints - .hintsForKey(replyToAuthor.pubkeyHex) - .ifEmpty { null } - ?.toSet() - ?: emptySet() - } - } else { - emptySet() - } - - existingRelays + replyAuthorRelays - } ?: emptySet() - - return authorOutboxRelays + taggedUserInboxRelays + channelRelays + replyRelays + val noteEvent = note.event + return if (noteEvent != null) { + computeRelayListToBroadcast(noteEvent) + } else { + note.relays.toSet() + } } fun broadcast(note: Note) { @@ -1735,37 +1730,8 @@ class Account( cache.getOrAddAliasNote(wrap.id, mineNote) } - val receiver = wrap.recipientPubKey() - if (receiver != null) { - val relayList = - ( - cache - .getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(receiver)) - ?.event as? ChatMessageRelayListEvent - )?.relays()?.ifEmpty { null }?.toSet() - - if (relayList != null) { - client.send(event = wrap, relayList = relayList) - } else { - val taggedUserInboxRelays = - wrap.taggedUserIds().flatMapTo(mutableSetOf()) { pubkey -> - if (pubkey == userProfile().pubkeyHex) { - notificationRelays.flow.value - } else { - LocalCache - .getUserIfExists(pubkey) - ?.inboxRelays() - ?.ifEmpty { null } - ?.toSet() - ?: cache.relayHints.hintsForKey(pubkey).toSet() - } - } - - client.send(wrap, taggedUserInboxRelays) - } - } else { - client.send(wrap, outboxRelays.flow.value) - } + val relayList = computeRelayListToBroadcast(wrap) + client.send(wrap, relayList) } } From 0be9447bee3cd4856f9d345c4c46bc3a5f539bc2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 15:22:09 -0400 Subject: [PATCH 048/185] Reusing the parsed event to reuse cache when computing relay hints. --- .../com/vitorpamplona/amethyst/model/LocalCache.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 3b4dc9fd37..550c86fc8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2997,7 +2997,17 @@ object LocalCache : ILocalCache { } if (relay != null) { - addIncomingRelayAsHintToAllRelatedEvents(event, relay) + // uses the internal event to avoid reprocessing cached items. + val note = + if (event is AddressableEvent) { + getAddressableNoteIfExists(event.address()) + } else { + getNoteIfExists(event.id) + } + + note?.event?.let { consumedEvent -> + addIncomingRelayAsHintToAllRelatedEvents(consumedEvent, relay) + } } return wasNew From dbb9ee5379f8790703849660e569dc35fa2ba1ba Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 15:22:31 -0400 Subject: [PATCH 049/185] Removing all the default since logic --- .../subassemblies/FilterLongFormByAllCommunities.kt | 5 +---- .../subassemblies/FilterLongFormByAuthors.kt | 9 ++------- .../subassemblies/FilterLongFormByCommunity.kt | 5 +---- .../subassemblies/FilterLongFormByFollows.kt | 5 +---- .../subassemblies/FilterLongFormByGeohash.kt | 5 +---- .../subassemblies/FilterLongFormByHashtag.kt | 5 +---- .../nip23LongForm/subassemblies/FilterLongFormGlobal.kt | 5 +---- .../subassemblies/FilterPublicChatsByAllCommunities.kt | 6 +----- .../subassemblies/FilterPublicChatsByAuthors.kt | 9 ++------- .../subassemblies/FilterPublicChatsByCommunity.kt | 5 +---- .../subassemblies/FilterPublicChatsByFollows.kt | 5 +---- .../subassemblies/FilterPublicChatsByGeohash.kt | 5 +---- .../subassemblies/FilterPublicChatsByHashtag.kt | 5 +---- .../nip28Chats/subassemblies/FilterPublicChatsGlobal.kt | 5 +---- .../subassemblies/FilterFollowSetsByAllCommunities.kt | 5 +---- .../subassemblies/FilterFollowSetsByAuthors.kt | 9 ++------- .../subassemblies/FilterFollowSetsByCommunity.kt | 5 +---- .../subassemblies/FilterFollowSetsByFollows.kt | 5 +---- .../subassemblies/FilterFollowSetsByGeohash.kt | 5 +---- .../subassemblies/FilterFollowSetsByHashtag.kt | 5 +---- .../subassemblies/FilterFollowSetsGlobal.kt | 5 +---- .../FilterLiveActivitiesByAllCommunities.kt | 5 +---- .../subassemblies/FilterLiveActivitiesByAuthors.kt | 9 ++------- .../subassemblies/FilterLiveActivitiesByCommunity.kt | 5 +---- .../subassemblies/FilterLiveActivitiesByFollows.kt | 5 +---- .../subassemblies/FilterLiveActivitiesByGeohash.kt | 5 +---- .../subassemblies/FilterLiveActivitiesByHashtag.kt | 5 +---- .../subassemblies/FilterLiveActivitiesGlobal.kt | 5 +---- .../subassemblies/FilterCommunitiesByAllCommunities.kt | 5 +---- .../subassemblies/FilterCommunitiesByAuthors.kt | 9 ++------- .../subassemblies/FilterCommunitiesByCommunity.kt | 5 +---- .../subassemblies/FilterCommunitiesByFollows.kt | 5 +---- .../subassemblies/FilterCommunitiesByGeohash.kt | 5 +---- .../subassemblies/FilterCommunitiesByHashtag.kt | 5 +---- .../subassemblies/FilterCommunitiesGlobal.kt | 5 +---- .../subassemblies/FilterContentDVMsByAllCommunities.kt | 5 +---- .../subassemblies/FilterContentDVMsByAuthors.kt | 9 ++------- .../subassemblies/FilterContentDVMsByCommunity.kt | 5 +---- .../subassemblies/FilterContentDVMsByFollows.kt | 5 +---- .../subassemblies/FilterContentDVMsByGeohash.kt | 5 +---- .../subassemblies/FilterContentDVMsByHashtag.kt | 5 +---- .../nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt | 5 +---- .../subassemblies/FilterClassifiedsByAllCommunities.kt | 5 +---- .../subassemblies/FilterClassifiedsByAuthors.kt | 9 ++------- .../subassemblies/FilterClassifiedsByCommunity.kt | 5 +---- .../subassemblies/FilterClassifiedsByFollows.kt | 5 +---- .../subassemblies/FilterClassifiedsByGeohash.kt | 5 +---- .../subassemblies/FilterClassifiedsByHashtag.kt | 5 +---- .../subassemblies/FilterClassifiedsGlobal.kt | 5 +---- .../datasource/nip01Core/FilterHomePostsByGeohashes.kt | 7 ++----- .../home/datasource/nip01Core/FilterHomePostsByGlobal.kt | 5 +---- .../datasource/nip01Core/FilterHomePostsByHashtags.kt | 5 +---- .../datasource/nip65Follows/FilterHomePostsByAuthors.kt | 9 ++------- .../nip72Communities/FilterHomePostsByAllCommunities.kt | 5 +---- .../nip72Communities/FilterHomePostsFromCommunities.kt | 5 +---- .../nip01Core/FilterPictureAndVideoByHashtag.kt | 5 +---- .../nip65Follows/FilterPictureAndVideoByAuthors.kt | 9 ++------- .../nip72Communities/FilterPictureAndVideoByCommunity.kt | 5 +---- 58 files changed, 68 insertions(+), 261 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt index 1d62339373..e7aedaf0b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterLongFormAllCommunities( @@ -73,14 +72,12 @@ fun filterLongFormByAllCommunities( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterLongFormAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt index 40f3f914de..73355dc066 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull @@ -58,8 +57,6 @@ fun filterLongFormByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -68,7 +65,7 @@ fun filterLongFormByAuthors( filterLongFormAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() @@ -80,8 +77,6 @@ fun filterLongFormByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -90,7 +85,7 @@ fun filterLongFormByAuthors( filterLongFormAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt index 7cb866ad01..bd57e55d9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterLongFormByCommunity( @@ -75,15 +74,13 @@ fun filterLongFormByCommunity( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterLongFormByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt index c3e1a7d47f..ea986583c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.sub import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatMap fun filterLongFormByFollows( @@ -32,10 +31,8 @@ fun filterLongFormByFollows( ): List { if (followsSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return followsSet.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt index 138a4fca2d..b0bedecce1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterLongFormByGeohash( @@ -59,8 +58,6 @@ fun filterLongFormByGeohash( ): List { if (geoSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set .mapNotNull { if (it.value.geotags.isEmpty()) { @@ -69,7 +66,7 @@ fun filterLongFormByGeohash( filterLiveActivitiesByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt index 07c36e21be..e197ce8c2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterLongFormByHashtag( @@ -59,8 +58,6 @@ fun filterLongFormByHashtag( ): List { if (hashSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set .mapNotNull { relayHashSet -> if (relayHashSet.value.hashtags.isEmpty()) { @@ -69,7 +66,7 @@ fun filterLongFormByHashtag( filterLongFormByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, + since = since?.get(relayHashSet.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt index c5445f7f21..026affbdd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.utils.TimeUtils fun filterLongFormGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -33,10 +32,8 @@ fun filterLongFormGlobal( ): List { if (relays.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return relays.set.map { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time RelayBasedFilter( relay = it.key, filter = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt index 656e396f33..518dfe54c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterPublicChatsAllCommunities( @@ -79,15 +78,12 @@ fun filterPublicChatsByAllCommunities( since: SincePerRelayMap?, ): List { if (communitySet.set.isEmpty()) return emptyList() - - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterPublicChatsAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt index aa3a7a7f8a..c40160aecd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt @@ -30,7 +30,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull @@ -65,8 +64,6 @@ fun filterPublicChatsByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -75,7 +72,7 @@ fun filterPublicChatsByAuthors( filterPublicChatsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() @@ -87,8 +84,6 @@ fun filterPublicChatsByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -97,7 +92,7 @@ fun filterPublicChatsByAuthors( filterPublicChatsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt index 98d183db45..690ae70b11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterPublicChatsByCommunity( @@ -82,15 +81,13 @@ fun filterPublicChatsByCommunity( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterPublicChatsByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt index 1c57ba3741..c71864df9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subass import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatMap fun filterPublicChatsByFollows( @@ -32,10 +31,8 @@ fun filterPublicChatsByFollows( ): List { if (followsSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return followsSet.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt index 2e03857e5d..f41fe12280 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterPublicChatsByGeohash( @@ -65,8 +64,6 @@ fun filterPublicChatsByGeohash( ): List { if (geoSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set .mapNotNull { if (it.value.geotags.isEmpty()) { @@ -75,7 +72,7 @@ fun filterPublicChatsByGeohash( filterPublicChatsByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt index 1f70a7a73b..f71ecdb885 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterPublicChatsByHashtag( @@ -66,8 +65,6 @@ fun filterPublicChatsByHashtag( ): List { if (hashSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set .mapNotNull { relayHashSet -> if (relayHashSet.value.hashtags.isEmpty()) { @@ -76,7 +73,7 @@ fun filterPublicChatsByHashtag( filterPublicChatsByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, + since = since?.get(relayHashSet.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt index 3b4062471f..d1bb4587d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.utils.TimeUtils fun filterPublicChatsGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -35,10 +34,8 @@ fun filterPublicChatsGlobal( ): List { if (relays.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return relays.set.map { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time RelayBasedFilter( relay = it.key, filter = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt index 93ca8f70e6..eb8c9bc958 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterFollowSetsAllCommunities( @@ -74,14 +73,12 @@ fun filterFollowSetsByAllCommunities( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterContentDVMsAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt index 3d08502c2a..032303bf30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull @@ -58,8 +57,6 @@ fun filterFollowSetsByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -68,7 +65,7 @@ fun filterFollowSetsByAuthors( filterFollowSetsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() @@ -80,8 +77,6 @@ fun filterFollowSetsByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -90,7 +85,7 @@ fun filterFollowSetsByAuthors( filterFollowSetsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt index 09a9be8984..bf7f27e293 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterFollowSetsByCommunity( @@ -75,15 +74,13 @@ fun filterFollowSetsByCommunity( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterFollowSetsByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt index 60acbeabba..cdc321d1e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.s import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatMap fun filterFollowSetsByFollows( @@ -32,10 +31,8 @@ fun filterFollowSetsByFollows( ): List { if (followsSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return followsSet.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt index 21195dd0ec..5f27987d25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterFollowSetsByGeohash( @@ -58,8 +57,6 @@ fun filterFollowSetsByGeohash( ): List { if (geoSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set .mapNotNull { if (it.value.geotags.isEmpty()) { @@ -68,7 +65,7 @@ fun filterFollowSetsByGeohash( filterFollowSetsByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt index f5e8e4caf0..93d638c066 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterFollowSetsByHashtag( @@ -59,8 +58,6 @@ fun filterFollowSetsByHashtag( ): List { if (hashSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set .mapNotNull { relayHashSet -> if (relayHashSet.value.hashtags.isEmpty()) { @@ -69,7 +66,7 @@ fun filterFollowSetsByHashtag( filterFollowSetsByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, + since = since?.get(relayHashSet.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt index ebc2943481..17223c80c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.utils.TimeUtils fun filterFollowSetsGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -33,10 +32,8 @@ fun filterFollowSetsGlobal( ): List { if (relays.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return relays.set.map { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time RelayBasedFilter( relay = it.key, filter = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt index 6354b18c61..1d1d7b2d76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterLiveActivitiesAllCommunities( @@ -75,14 +74,12 @@ fun filterLiveActivitiesByAllCommunities( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterContentDVMsAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt index 8d32112266..7bbb51135f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull @@ -70,8 +69,6 @@ fun filterLiveActivitiesByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -80,7 +77,7 @@ fun filterLiveActivitiesByAuthors( filterLiveActivitiesAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() @@ -92,8 +89,6 @@ fun filterLiveActivitiesByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -102,7 +97,7 @@ fun filterLiveActivitiesByAuthors( filterLiveActivitiesAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt index 0141bb8b70..dc4499e277 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterLiveActivitiesByCommunity( @@ -76,15 +75,13 @@ fun filterLiveActivitiesByCommunity( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterLiveActivitiesByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt index 40ee82d9b1..bbce0a5227 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActiviti import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatMap fun filterLiveActivitiesByFollows( @@ -32,10 +31,8 @@ fun filterLiveActivitiesByFollows( ): List { if (followsSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return followsSet.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt index 875bf036b8..fc0c789348 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterLiveActivitiesByGeohash( @@ -59,8 +58,6 @@ fun filterLiveActivitiesByGeohash( ): List { if (geoSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set .mapNotNull { if (it.value.geotags.isEmpty()) { @@ -69,7 +66,7 @@ fun filterLiveActivitiesByGeohash( filterLiveActivitiesByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt index 5e7e7b4bdb..9d8be1f461 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterLiveActivitiesByHashtag( @@ -60,8 +59,6 @@ fun filterLiveActivitiesByHashtag( ): List { if (hashSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set .mapNotNull { relayHashSet -> if (relayHashSet.value.hashtags.isEmpty()) { @@ -70,7 +67,7 @@ fun filterLiveActivitiesByHashtag( filterLiveActivitiesByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, + since = since?.get(relayHashSet.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt index 5b9c7edd54..c49b471e3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.utils.TimeUtils fun filterLiveActivitiesGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -34,10 +33,8 @@ fun filterLiveActivitiesGlobal( ): List { if (relays.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return relays.set.map { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time RelayBasedFilter( relay = it.key, filter = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt index 07f1637a0c..7920bfb56b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterCommunitiesAllCommunities( @@ -69,14 +68,12 @@ fun filterCommunitiesByAllCommunities( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterCommunitiesAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt index 7557077b84..44f4fbd0d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull @@ -59,8 +58,6 @@ fun filterCommunitiesByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -69,7 +66,7 @@ fun filterCommunitiesByAuthors( filterCommunitiesAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() @@ -81,8 +78,6 @@ fun filterCommunitiesByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -91,7 +86,7 @@ fun filterCommunitiesByAuthors( filterCommunitiesAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt index 6697fd72c2..062a4b5a75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterClassifiedsByCommunity( @@ -58,15 +57,13 @@ fun filterCommunitiesByCommunity( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterClassifiedsByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt index 014ac2d36f..b4719b5abc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities. import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatMap fun filterCommunitiesByFollows( @@ -32,10 +31,8 @@ fun filterCommunitiesByFollows( ): List { if (followsSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return followsSet.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt index a3f87648d8..1f3bf415c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterCommunitiesByGeohash( @@ -59,8 +58,6 @@ fun filterCommunitiesByGeohash( ): List { if (geoSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set .mapNotNull { if (it.value.geotags.isEmpty()) { @@ -69,7 +66,7 @@ fun filterCommunitiesByGeohash( filterCommunitiesByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt index 59dc98a02c..2e88e90a8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterCommunitiesByHashtag( @@ -60,8 +59,6 @@ fun filterCommunitiesByHashtag( ): List { if (hashSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set .mapNotNull { relayHashSet -> if (relayHashSet.value.hashtags.isEmpty()) { @@ -70,7 +67,7 @@ fun filterCommunitiesByHashtag( filterCommunitiesByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, + since = since?.get(relayHashSet.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt index 68e42b47a4..4835e9d21b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils fun filterCommunitiesGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -34,10 +33,8 @@ fun filterCommunitiesGlobal( ): List { if (relays.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return relays.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time listOf( RelayBasedFilter( relay = it.key, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt index 0af5469578..43bf3702fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterContentDVMsAllCommunities( @@ -73,14 +72,12 @@ fun filterContentDVMsByAllCommunities( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterContentDVMsAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt index cd5769e97c..bf5f9eec91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull @@ -59,8 +58,6 @@ fun filterContentDVMsByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -69,7 +66,7 @@ fun filterContentDVMsByAuthors( filterContentDVMsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() @@ -81,8 +78,6 @@ fun filterContentDVMsByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -91,7 +86,7 @@ fun filterContentDVMsByAuthors( filterContentDVMsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt index 1cdb07be4f..1413d5a106 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterContentDVMsByCommunity( @@ -75,15 +74,13 @@ fun filterContentDVMsByCommunity( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterContentDVMsByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt index 6cf4306250..cf5c2527f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subasse import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatMap fun filterContentDVMsByFollows( @@ -32,10 +31,8 @@ fun filterContentDVMsByFollows( ): List { if (followsSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return followsSet.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt index 23f3342b39..dad6a5ea53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterContentDVMsByGeohash( @@ -58,8 +57,6 @@ fun filterContentDVMsByGeohash( ): List { if (geoSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set .mapNotNull { if (it.value.geotags.isEmpty()) { @@ -68,7 +65,7 @@ fun filterContentDVMsByGeohash( filterContentDVMsByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt index 615c014384..876a9bf3e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterContentDVMsByHashtag( @@ -59,8 +58,6 @@ fun filterContentDVMsByHashtag( ): List { if (hashSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set .mapNotNull { relayHashSet -> if (relayHashSet.value.hashtags.isEmpty()) { @@ -69,7 +66,7 @@ fun filterContentDVMsByHashtag( filterContentDVMsByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, + since = since?.get(relayHashSet.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt index 2f44bd1999..317aa48578 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.utils.TimeUtils fun filterContentDVMsGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -33,10 +32,8 @@ fun filterContentDVMsGlobal( ): List { if (relays.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return relays.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time listOf( RelayBasedFilter( relay = it.key, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt index 3ba7cd6193..5d372b2bc9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterClassifiedsAllCommunities( @@ -73,14 +72,12 @@ fun filterClassifiedsByAllCommunities( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterClassifiedsAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt index 838c6f04e6..7b399a45f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull @@ -58,8 +57,6 @@ fun filterClassifiedsByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -68,7 +65,7 @@ fun filterClassifiedsByAuthors( filterClassifiedsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() @@ -80,8 +77,6 @@ fun filterClassifiedsByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -90,7 +85,7 @@ fun filterClassifiedsByAuthors( filterClassifiedsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt index 1ee093d0cc..77a50c174d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterClassifiedsByCommunity( @@ -75,15 +74,13 @@ fun filterClassifiedsByCommunity( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterClassifiedsByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt index 0a6924668f..b8613876e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds. import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterClassifiedsByFollows( @@ -32,10 +31,8 @@ fun filterClassifiedsByFollows( ): List { if (followsSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return followsSet.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt index 5225b0a2f4..e0a5b7756a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterClassifiedsByGeohash( @@ -58,8 +57,6 @@ fun filterClassifiedsByGeohash( ): List { if (geoSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set .mapNotNull { if (it.value.geotags.isEmpty()) { @@ -68,7 +65,7 @@ fun filterClassifiedsByGeohash( filterClassifiedsByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt index 8aa3322181..f9d68f0469 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull fun filterClassifiedsByHashtag( @@ -59,8 +58,6 @@ fun filterClassifiedsByHashtag( ): List { if (hashSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set .mapNotNull { relayHashSet -> if (relayHashSet.value.hashtags.isEmpty()) { @@ -69,7 +66,7 @@ fun filterClassifiedsByHashtag( filterClassifiedsByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, + since = since?.get(relayHashSet.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt index 57c0f48397..b711f3e0a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils fun filterClassifiedsGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -33,10 +32,8 @@ fun filterClassifiedsGlobal( ): List { if (relays.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return relays.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time listOf( RelayBasedFilter( relay = it.key, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt index e6b332fcea..8d14c4423f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt @@ -35,7 +35,6 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull import kotlin.collections.sorted @@ -80,8 +79,6 @@ fun filterHomePostsByGeohashes( ): List { if (geoSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return geoSet.set .mapNotNull { if (it.value.geotags.isEmpty()) { @@ -90,12 +87,12 @@ fun filterHomePostsByGeohashes( filterHomePostsByGeohashes( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) + filterHomePostsByScopes( relay = it.key, scopesToLoad = it.value.geotagScopes, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt index 98d6cc60e4..1a6cc8e689 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt @@ -39,7 +39,6 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEven import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils val HomePostsByGlobalKinds = listOf( @@ -70,10 +69,8 @@ fun filterHomePostsByGlobal( ): List { if (relays.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return relays.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time val relayUrl = it.key listOf( RelayBasedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt index fb65c4851d..602d2aca3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt @@ -37,7 +37,6 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull @@ -84,14 +83,12 @@ fun filterHomePostsByHashtags( ): List { if (hashtagSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return hashtagSet.set .mapNotNull { if (it.value.hashtags.isEmpty()) { null } else { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time return filterHomePostsByHashtags( relay = it.key, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt index 4fd05a4bb3..a823073eeb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -42,7 +42,6 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEven import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.mapNotNull val HomePostsKinds = @@ -105,8 +104,6 @@ fun filterHomePostsByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -115,7 +112,7 @@ fun filterHomePostsByAuthors( filterHomePostsByAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() @@ -127,8 +124,6 @@ fun filterHomePostsByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -137,7 +132,7 @@ fun filterHomePostsByAuthors( filterHomePostsByAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt index 0eaf28b7fd..9376353807 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterHomePostsFromAllCommunities( @@ -72,14 +71,12 @@ fun filterHomePostsByAllCommunities( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterHomePostsFromAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt index 30a90c8578..7ff8d1db43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt @@ -33,7 +33,6 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull @@ -106,15 +105,13 @@ fun filterHomePostsByCommunity( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterHomePostsFromCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt index 7c40f44526..259a1470f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterPictureAndVideoHashtag( @@ -73,8 +72,6 @@ fun filterPictureAndVideoByHashtag( ): List { if (hashSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return hashSet.set .mapNotNull { relayHashSet -> if (relayHashSet.value.hashtags.isEmpty()) { @@ -83,7 +80,7 @@ fun filterPictureAndVideoByHashtag( filterPictureAndVideoHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time ?: defaultSince, + since = since?.get(relayHashSet.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt index 25e1423955..3ba1d739e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt @@ -30,7 +30,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten import kotlin.collections.mapNotNull @@ -71,8 +70,6 @@ fun filterPictureAndVideoByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -81,7 +78,7 @@ fun filterPictureAndVideoByAuthors( filterPictureAndVideoAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() @@ -93,8 +90,6 @@ fun filterPictureAndVideoByAuthors( ): List { if (authorSet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return authorSet.set .mapNotNull { if (it.value.authors.isEmpty()) { @@ -103,7 +98,7 @@ fun filterPictureAndVideoByAuthors( filterPictureAndVideoAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt index a6c532b049..2dca47d3fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.flatten fun filterPictureAndVideoCommunity( @@ -111,15 +110,13 @@ fun filterPictureAndVideoByCommunity( ): List { if (communitySet.set.isEmpty()) return emptyList() - val defaultSince = TimeUtils.oneWeekAgo() - return communitySet.set .mapNotNull { filterPictureAndVideoCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time ?: defaultSince, + since = since?.get(it.key)?.time, ) }.flatten() } From 8d14f3615d1b73970d45aa1519a78ee1d5cae902 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 15:22:55 -0400 Subject: [PATCH 050/185] Improves Bech32 parser to consider only the 58 chars of nsec1, npub1 and note1. --- .../quartz/nip19Bech32/Nip19Parser.kt | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt index 4afd159f6c..a598bb1d27 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/Nip19Parser.kt @@ -43,13 +43,13 @@ import java.util.regex.Pattern object Nip19Parser { private val nip19PlusNip46regex: Pattern = Pattern.compile( - "(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1|nembed1|ncryptsec1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)([\\S]*)", + "(nostr:)?@?((nsec1|npub1|note1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58})|(nevent1|naddr1|nprofile1|nrelay1|nembed1|ncryptsec1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+))([\\S]*)", Pattern.CASE_INSENSITIVE, ) val nip19regex: Pattern = Pattern.compile( - "(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1|nembed1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)([\\S]*)", + "(nostr:)?@?((nsec1|npub1|note1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58})|(nevent1|naddr1|nprofile1|nrelay1|nembed1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+))([\\S]*)", Pattern.CASE_INSENSITIVE, ) @@ -75,8 +75,8 @@ object Nip19Parser { return null } - val type = matcher.group(2) // npub1 - val key = matcher.group(3) // bech32 + val type = matcher.group(3) ?: matcher.group(5) // npub1 + val key = matcher.group(4) ?: matcher.group(6) // bech32 return type!! + key } catch (e: Throwable) { @@ -95,9 +95,9 @@ object Nip19Parser { return null } - val type = matcher.group(2) // npub1 - val key = matcher.group(3) // bech32 - val additionalChars = matcher.group(4) // additional chars + val type = matcher.group(3) ?: matcher.group(5) // npub1 + val key = matcher.group(4) ?: matcher.group(6) // bech32 + val additionalChars = matcher.group(7) // additional chars if (type == null) return null @@ -136,11 +136,27 @@ object Nip19Parser { null } - fun parseAll( - content: String, - regex: Pattern, - ): List { - val matcher2 = regex.matcher(content) + fun parseAll(content: String): List { + val matcher = nip19regex.matcher(content) + val returningList = mutableListOf() + while (matcher.find()) { + val type = matcher.group(3) ?: matcher.group(5) // npub1 + val key = matcher.group(4) ?: matcher.group(6) // bech32 + val additionalChars = matcher.group(7) // additional chars + + if (type != null) { + val parsed = parseComponents(type, key, additionalChars)?.entity + + if (parsed != null) { + returningList.add(parsed) + } + } + } + return returningList + } + + fun parseAllEvents(content: String): List { + val matcher2 = nip19regexEvents.matcher(content) val returningList = mutableListOf() while (matcher2.find()) { val type = matcher2.group(2) // npub1 @@ -157,10 +173,6 @@ object Nip19Parser { } return returningList } - - fun parseAll(content: String): List = parseAll(content, nip19regex) - - fun parseAllEvents(content: String): List = parseAll(content, nip19regexEvents) } fun decodePublicKey(key: String): ByteArray = From e89a04eb569a3d646e10f5192f281f5806dbc184 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 17:21:35 -0400 Subject: [PATCH 051/185] Makes sure the metadata and the Advertised Relay list goes to every connected relay. --- .../main/java/com/vitorpamplona/amethyst/model/Account.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index afac211c35..365d18c8a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -101,6 +101,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.downloadFirstEvent import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient @@ -174,6 +175,7 @@ import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip68Picture.PictureMeta @@ -844,6 +846,9 @@ class Account( } fun computeRelayListToBroadcast(event: Event): Set { + if (event is MetadataEvent || event is AdvertisedRelayListEvent) { + return followPlusAllMine.flow.value + client.relayStatusFlow().value.available + } if (event is GiftWrapEvent) { val receiver = event.recipientPubKey() if (receiver != null) { From 8c4ec1dc7da785b73fa6a878a5b27fc77f5f4ded Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 17:25:52 -0400 Subject: [PATCH 052/185] Includes Trusted Relays in all my relays --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../MergedFollowPlusMineRelayListsState.kt | 49 ++++++++++--------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 365d18c8a7..4fcb056b77 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -277,7 +277,7 @@ class Account( // Follows Relays val followOutboxes = FollowListOutboxRelays(kind3FollowList, blockedRelayList, cache, scope) - val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, scope) + val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, trustedRelayList, localRelayList, scope) // keeps a cache of the outbox relays for each author val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index 1fdb932662..ac719c9d1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays +import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope @@ -39,32 +40,33 @@ class MergedFollowPlusMineRelayListsState( val nip65RelayList: Nip65RelayListState, val privateOutboxRelayList: PrivateStorageRelayListState, val localRelayList: LocalRelayListState, + val trustedRelayList: TrustedRelayListState, val scope: CoroutineScope, ) { - fun mergeLists( - kind3: Set, - outbox: Set, - inbox: Set, - private: Set, - local: Set, - ): Set = kind3 + outbox + inbox + private + local + fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } val flow: StateFlow> = combine( - followsOutboxRelayList.flow, - nip65RelayList.outboxFlow, - nip65RelayList.inboxFlow, - privateOutboxRelayList.flow, - localRelayList.flow, + listOf( + followsOutboxRelayList.flow, + nip65RelayList.outboxFlow, + nip65RelayList.inboxFlow, + privateOutboxRelayList.flow, + localRelayList.flow, + trustedRelayList.flow, + ), ::mergeLists, ).onStart { emit( mergeLists( - followsOutboxRelayList.flow.value, - nip65RelayList.outboxFlow.value, - nip65RelayList.inboxFlow.value, - privateOutboxRelayList.flow.value, - localRelayList.flow.value, + arrayOf( + followsOutboxRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + trustedRelayList.flow.value, + ), ), ) }.flowOn(Dispatchers.Default) @@ -72,11 +74,14 @@ class MergedFollowPlusMineRelayListsState( scope, SharingStarted.Eagerly, mergeLists( - followsOutboxRelayList.flow.value, - nip65RelayList.outboxFlow.value, - nip65RelayList.inboxFlow.value, - privateOutboxRelayList.flow.value, - localRelayList.flow.value, + arrayOf( + followsOutboxRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + trustedRelayList.flow.value, + ), ), ) } From 8e0b94646ad556006818cb6081ae890026053745 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 17:25:52 -0400 Subject: [PATCH 053/185] Includes Trusted Relays in all my relays --- .../vitorpamplona/amethyst/model/Account.kt | 6 ++- .../MergedFollowPlusMineRelayListsState.kt | 49 ++++++++++--------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 365d18c8a7..6ad8d1a928 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -61,7 +61,6 @@ import com.vitorpamplona.amethyst.model.serverList.MergedServerListState import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter -import com.vitorpamplona.amethyst.model.topNavFeeds.MergedTopFeedAuthorListsState import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState import com.vitorpamplona.amethyst.model.torState.TorRelayState import com.vitorpamplona.amethyst.service.location.LocationState @@ -277,7 +276,7 @@ class Account( // Follows Relays val followOutboxes = FollowListOutboxRelays(kind3FollowList, blockedRelayList, cache, scope) - val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, scope) + val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, trustedRelayList, scope) // keeps a cache of the outbox relays for each author val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow @@ -338,6 +337,7 @@ class Account( val liveNotificationFollowListsPerRelay = OutboxLoaderState(liveNotificationFollowLists, cache, scope).flow + /* val mergedTopFeedAuthorLists = MergedTopFeedAuthorListsState( liveHomeFollowListsPerRelay, @@ -347,6 +347,8 @@ class Account( scope, ).flow + */ + val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope) fun decryptPeopleList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index 1fdb932662..ac719c9d1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays +import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope @@ -39,32 +40,33 @@ class MergedFollowPlusMineRelayListsState( val nip65RelayList: Nip65RelayListState, val privateOutboxRelayList: PrivateStorageRelayListState, val localRelayList: LocalRelayListState, + val trustedRelayList: TrustedRelayListState, val scope: CoroutineScope, ) { - fun mergeLists( - kind3: Set, - outbox: Set, - inbox: Set, - private: Set, - local: Set, - ): Set = kind3 + outbox + inbox + private + local + fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } val flow: StateFlow> = combine( - followsOutboxRelayList.flow, - nip65RelayList.outboxFlow, - nip65RelayList.inboxFlow, - privateOutboxRelayList.flow, - localRelayList.flow, + listOf( + followsOutboxRelayList.flow, + nip65RelayList.outboxFlow, + nip65RelayList.inboxFlow, + privateOutboxRelayList.flow, + localRelayList.flow, + trustedRelayList.flow, + ), ::mergeLists, ).onStart { emit( mergeLists( - followsOutboxRelayList.flow.value, - nip65RelayList.outboxFlow.value, - nip65RelayList.inboxFlow.value, - privateOutboxRelayList.flow.value, - localRelayList.flow.value, + arrayOf( + followsOutboxRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + trustedRelayList.flow.value, + ), ), ) }.flowOn(Dispatchers.Default) @@ -72,11 +74,14 @@ class MergedFollowPlusMineRelayListsState( scope, SharingStarted.Eagerly, mergeLists( - followsOutboxRelayList.flow.value, - nip65RelayList.outboxFlow.value, - nip65RelayList.inboxFlow.value, - privateOutboxRelayList.flow.value, - localRelayList.flow.value, + arrayOf( + followsOutboxRelayList.flow.value, + nip65RelayList.outboxFlow.value, + nip65RelayList.inboxFlow.value, + privateOutboxRelayList.flow.value, + localRelayList.flow.value, + trustedRelayList.flow.value, + ), ), ) } From 4084a721784678181dfce080a58501ed28cf754c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 18:17:18 -0400 Subject: [PATCH 054/185] Minor refactoring in Account --- .../vitorpamplona/amethyst/model/Account.kt | 85 +++---------------- .../edits/PrivateStorageRelayListState.kt | 1 + .../model/nip17Dms/DmRelayListState.kt | 2 + .../model/nip50Search/SearchRelayListState.kt | 1 + .../model/nip51Lists/BlockedRelayListState.kt | 1 + .../model/nip51Lists/TrustedRelayListState.kt | 1 + .../FileStorageServerListState.kt | 1 + .../nipB7Blossom/BlossomServerListState.kt | 33 +++++++ 8 files changed, 52 insertions(+), 73 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 6ad8d1a928..698198ed9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -195,7 +195,6 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent -import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -229,7 +228,7 @@ import kotlin.coroutines.resume class Account( val settings: AccountSettings = AccountSettings(KeyPair()), val signer: NostrSigner = settings.createSigner(), - val geolocationFlow: StateFlow, + geolocationFlow: StateFlow, val cache: LocalCache, val client: NostrClient, val scope: CoroutineScope, @@ -698,28 +697,12 @@ class Account( hash: HexKey, size: Long, alt: String, - ): BlossomAuthorizationEvent? { - if (!isWriteable()) return null - - return tryAndWait { continuation -> - BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, signer) { - continuation.resume(it) - } - } - } + ) = blossomServers.createBlossomUploadAuth(hash, size, alt) suspend fun createBlossomDeleteAuth( hash: HexKey, alt: String, - ): BlossomAuthorizationEvent? { - if (!isWriteable()) return null - - return tryAndWait { continuation -> - BlossomAuthorizationEvent.createDeleteAuth(hash, alt, signer) { - continuation.resume(it) - } - } - } + ) = blossomServers.createBlossomDeleteAuth(hash, alt) suspend fun boost(note: Note) { RepostAction.repost(note, signer) { @@ -1849,31 +1832,11 @@ class Account( relay: IRelayClient, challenge: String, ) { - createAuthEvent(relay.url, challenge) { + RelayAuthEvent.create(relay.url, challenge, signer) { client.sendIfExists(it, relay.url) } } - fun createAuthEvent( - relayUrl: NormalizedRelayUrl, - challenge: String, - onReady: (RelayAuthEvent) -> Unit, - ) { - if (!isWriteable()) return - - RelayAuthEvent.create(relayUrl, challenge, signer, onReady = onReady) - } - - fun createAuthEvent( - relayUrls: List, - challenge: String, - onReady: (RelayAuthEvent) -> Unit, - ) { - if (!isWriteable()) return - - RelayAuthEvent.create(relayUrls, challenge, signer, onReady = onReady) - } - fun isInPrivateBookmarks( note: Note, onReady: (Boolean) -> Unit, @@ -2096,45 +2059,21 @@ class Account( ).toSet() } - fun saveDMRelayList(dmRelays: List) { - if (!isWriteable()) return - dmRelayList.saveRelayList(dmRelays, ::sendMyPublicAndPrivateOutbox) - } + fun saveDMRelayList(dmRelays: List) = dmRelayList.saveRelayList(dmRelays, ::sendMyPublicAndPrivateOutbox) - fun savePrivateOutboxRelayList(relays: List) { - if (!isWriteable()) return - privateStorageRelayList.saveRelayList(relays, ::sendMyPublicAndPrivateOutbox) - } + fun savePrivateOutboxRelayList(relays: List) = privateStorageRelayList.saveRelayList(relays, ::sendMyPublicAndPrivateOutbox) - fun saveSearchRelayList(searchRelays: List) { - if (!isWriteable()) return - searchRelayList.saveRelayList(searchRelays, ::sendMyPublicAndPrivateOutbox) - } + fun saveSearchRelayList(searchRelays: List) = searchRelayList.saveRelayList(searchRelays, ::sendMyPublicAndPrivateOutbox) - fun saveTrustedRelayList(trustedRelays: List) { - if (!isWriteable()) return - trustedRelayList.saveRelayList(trustedRelays, ::sendMyPublicAndPrivateOutbox) - } + fun saveTrustedRelayList(trustedRelays: List) = trustedRelayList.saveRelayList(trustedRelays, ::sendMyPublicAndPrivateOutbox) - fun saveBlockedRelayList(blockedRelays: List) { - if (!isWriteable()) return - blockedRelayList.saveRelayList(blockedRelays, ::sendMyPublicAndPrivateOutbox) - } + fun saveBlockedRelayList(blockedRelays: List) = blockedRelayList.saveRelayList(blockedRelays, ::sendMyPublicAndPrivateOutbox) - fun sendNip65RelayList(relays: List) { - if (!isWriteable()) return - nip65RelayList.saveRelayList(relays, ::sendMyPublicAndPrivateOutbox) - } + fun sendNip65RelayList(relays: List) = nip65RelayList.saveRelayList(relays, ::sendMyPublicAndPrivateOutbox) - fun sendFileServersList(servers: List) { - if (!isWriteable()) return - fileStorageServers.saveFileServersList(servers, ::sendMyPublicAndPrivateOutbox) - } + fun sendFileServersList(servers: List) = fileStorageServers.saveFileServersList(servers, ::sendMyPublicAndPrivateOutbox) - fun sendBlossomServersList(servers: List) { - if (!isWriteable()) return - blossomServers.saveBlossomServersList(servers, ::sendMyPublicAndPrivateOutbox) - } + fun sendBlossomServersList(servers: List) = blossomServers.saveBlossomServersList(servers, ::sendMyPublicAndPrivateOutbox) fun getAllPeopleLists(): List = getAllPeopleLists(signer.pubKey) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt index adec4d59bf..5c7510c8ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt @@ -75,6 +75,7 @@ class PrivateStorageRelayListState( relays: List, onDone: (PrivateOutboxRelayListEvent) -> Unit, ) { + if (!signer.isWriteable()) return val relayListForPrivateOutbox = getPrivateOutboxRelayList() if (relayListForPrivateOutbox != null && !relayListForPrivateOutbox.cachedPrivateTags().isNullOrEmpty()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt index 711ea3a933..d7be6fa974 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt @@ -75,6 +75,8 @@ class DmRelayListState( dmRelays: List, onDone: (ChatMessageRelayListEvent) -> Unit, ) { + if (!signer.isWriteable()) return + val relayListForDMs = getDMRelayList() if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) { ChatMessageRelayListEvent.updateRelayList( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt index 5f4a751061..5fc3202159 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt @@ -76,6 +76,7 @@ class SearchRelayListState( searchRelays: List, onDone: (SearchRelayListEvent) -> Unit, ) { + if (!signer.isWriteable()) return val relayListForSearch = getSearchRelayList() if (relayListForSearch != null && relayListForSearch.tags.isNotEmpty()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt index 14dcbbefc4..b897955254 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt @@ -75,6 +75,7 @@ class BlockedRelayListState( blockedRelays: List, onDone: (BlockedRelayListEvent) -> Unit, ) { + if (!signer.isWriteable()) return val relayListForBlocked = getBlockedRelayList() if (relayListForBlocked != null && relayListForBlocked.tags.isNotEmpty()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt index dc611b54fc..f1137b7bbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt @@ -75,6 +75,7 @@ class TrustedRelayListState( trustedRelays: List, onDone: (TrustedRelayListEvent) -> Unit, ) { + if (!signer.isWriteable()) return val relayListForTrusted = getTrustedRelayList() if (relayListForTrusted != null && relayListForTrusted.tags.isNotEmpty()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt index bccbc05725..943dad95f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt @@ -70,6 +70,7 @@ class FileStorageServerListState( servers: List, onDone: (FileServersEvent) -> Unit, ) { + if (!signer.isWriteable()) return val serverList = getFileServersList() val template = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt index a85941bade..69c910da80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt @@ -25,9 +25,12 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent +import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted @@ -36,6 +39,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlin.coroutines.resume class BlossomServerListState( val signer: NostrSigner, @@ -71,6 +75,8 @@ class BlossomServerListState( servers: List, onDone: (BlossomServersEvent) -> Unit, ) { + if (!signer.isWriteable()) return + val serverList = getBlossomServersList() if (serverList != null && serverList.tags.isNotEmpty()) { @@ -88,4 +94,31 @@ class BlossomServerListState( ) } } + + suspend fun createBlossomUploadAuth( + hash: HexKey, + size: Long, + alt: String, + ): BlossomAuthorizationEvent? { + if (!signer.isWriteable()) return null + + return tryAndWait { continuation -> + BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, signer) { + continuation.resume(it) + } + } + } + + suspend fun createBlossomDeleteAuth( + hash: HexKey, + alt: String, + ): BlossomAuthorizationEvent? { + if (!signer.isWriteable()) return null + + return tryAndWait { continuation -> + BlossomAuthorizationEvent.createDeleteAuth(hash, alt, signer) { + continuation.resume(it) + } + } + } } From c5ed0cfa60a1207699c404868bf21a32e307d7ac Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 18:43:10 -0400 Subject: [PATCH 055/185] Improves Image load screens in the preview cards --- .../amethyst/ui/components/MyAsyncImage.kt | 17 +++++++++--- .../ui/note/elements/DefaultImageHeader.kt | 26 +++++++++++++++++++ .../amethyst/ui/note/types/FollowList.kt | 17 ++++++++---- .../amethyst/ui/note/types/LiveActivity.kt | 2 ++ .../amethyst/ui/note/types/LongForm.kt | 2 ++ .../amethyst/ui/note/types/Video.kt | 2 ++ .../amethyst/ui/note/types/Wiki.kt | 2 ++ .../discover/nip51FollowSets/FollowSetCard.kt | 25 +++++++++++++----- .../loggedIn/threadview/ThreadFeedView.kt | 2 ++ 9 files changed, 80 insertions(+), 15 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt index 7a2e170754..398a4f4be1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.components +import android.R.attr.contentDescription import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.size @@ -35,6 +36,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import coil3.compose.AsyncImagePainter +import coil3.compose.AsyncImagePainter.State.Empty.painter import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImageContent import com.vitorpamplona.amethyst.model.MediaAspectRatioCache @@ -44,6 +46,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.Size6dp import com.vitorpamplona.amethyst.ui.theme.Size75dp +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError @Composable fun MyAsyncImage( @@ -53,7 +56,8 @@ fun MyAsyncImage( mainImageModifier: Modifier, loadedImageModifier: Modifier, accountViewModel: AccountViewModel, - onError: @Composable () -> Unit, + onLoadingBackground: (@Composable () -> Unit)?, + onError: (@Composable () -> Unit)?, ) { val ratio = MediaAspectRatioCache.get(imageUrl) val showImage = remember { mutableStateOf(accountViewModel.settings.showImages.value) } @@ -75,12 +79,19 @@ fun MyAsyncImage( } } else { WaitAndDisplay { - DisplayUrlWithLoadingSymbol(imageUrl) + Box(contentAlignment = Alignment.Center) { + if (onLoadingBackground != null) { + onLoadingBackground() + } + DisplayUrlWithLoadingSymbol(imageUrl) + } } } } is AsyncImagePainter.State.Error -> { - onError() + if (onError != null) { + onError() + } } is AsyncImagePainter.State.Success -> { SubcomposeAsyncImageContent(loadedImageModifier) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt index 6c1427bf14..62bca2ccef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -40,6 +41,7 @@ import com.vitorpamplona.amethyst.ui.note.WatchAuthor import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size16dp import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.authorNotePictureForImageHeader @@ -59,6 +61,22 @@ fun DefaultImageHeader( } } +@Composable +fun DefaultImageHeaderBackground( + note: Note, + accountViewModel: AccountViewModel, +) { + WatchAuthor(baseNote = note, accountViewModel) { + Box { + BannerImage(it, Modifier.fillMaxWidth().heightIn(max = 200.dp).blur(Size16dp), accountViewModel) + + Box(authorNotePictureForImageHeader.align(Alignment.BottomStart)) { + BaseUserPicture(it, Size55dp, accountViewModel, Modifier) + } + } + } +} + @Composable fun BannerImage( author: User, @@ -88,6 +106,14 @@ fun BannerImage( mainImageModifier = Modifier, loadedImageModifier = modifier, accountViewModel = accountViewModel, + onLoadingBackground = { + Image( + painter = painterRes(R.drawable.profile_banner, 4), + contentDescription = stringRes(R.string.profile_banner), + contentScale = ContentScale.Crop, + modifier = modifier, + ) + }, onError = { Image( painter = painterRes(R.drawable.profile_banner, 4), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt index e5938642cf..de9f74717c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text @@ -47,13 +46,15 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.UserCompose +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -87,16 +88,22 @@ fun DisplayFollowList( val image = noteEvent.image() image?.let { - AsyncImage( - model = it, + MyAsyncImage( + imageUrl = it, contentDescription = stringRes( R.string.preview_card_image_for, it, ), contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxWidth().padding(top = 5.dp).heightIn(max = 150.dp), + mainImageModifier = Modifier.fillMaxWidth(), + loadedImageModifier = Modifier, + accountViewModel = accountViewModel, + onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) }, + onError = { DefaultImageHeader(baseNote, accountViewModel) }, ) + } ?: run { + DefaultImageHeader(baseNote, accountViewModel) } Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index 853534e55a..d81c3fdd38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag @@ -232,6 +233,7 @@ fun RenderLiveActivityEventInner( mainImageModifier = Modifier.fillMaxWidth(), loadedImageModifier = MaterialTheme.colorScheme.imageModifier, accountViewModel = accountViewModel, + onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) }, onError = { DefaultImageHeader(baseNote, accountViewModel) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt index a07bf57d23..3defcc5679 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.QuoteBorder @@ -95,6 +96,7 @@ fun LongFormHeader( mainImageModifier = Modifier.fillMaxWidth(), loadedImageModifier = Modifier, accountViewModel = accountViewModel, + onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel) }, onError = { DefaultImageHeader(note, accountViewModel) }, ) } ?: run { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index 5b47a1473a..f9b7c9f2d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -53,6 +53,7 @@ import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -142,6 +143,7 @@ fun VideoDisplay( mainImageModifier = Modifier, loadedImageModifier = MaterialTheme.colorScheme.imageModifier, accountViewModel = accountViewModel, + onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel) }, onError = { DefaultImageHeader(note, accountViewModel) }, ) } ?: run { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt index e4cb083458..b130ee3277 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt @@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.QuoteBorder @@ -103,6 +104,7 @@ private fun WikiNoteHeader( mainImageModifier = Modifier, loadedImageModifier = MaterialTheme.colorScheme.imageModifier, accountViewModel = accountViewModel, + onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel) }, onError = { DefaultImageHeader(note, accountViewModel) }, ) } ?: run { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt index 298352ea46..034a455512 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt @@ -40,20 +40,23 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import coil3.compose.AsyncImage +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap +import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.ZapReaction +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp @@ -147,17 +150,25 @@ fun RenderFollowSetThumb( contentAlignment = Alignment.BottomStart, ) { card.media?.let { - AsyncImage( - model = it, - contentDescription = null, + MyAsyncImage( + imageUrl = it, + contentDescription = + stringRes( + R.string.preview_card_image_for, + it, + ), contentScale = ContentScale.Crop, - modifier = + mainImageModifier = Modifier.fillMaxWidth(), + loadedImageModifier = Modifier .fillMaxWidth() .aspectRatio(ratio = 21f / 9f) .clip(QuoteBorder), + accountViewModel = accountViewModel, + onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) }, + onError = { DefaultImageHeader(baseNote, accountViewModel) }, ) - } ?: run { DisplayAuthorBanner(baseNote, accountViewModel) } + } ?: run { DefaultImageHeader(baseNote, accountViewModel) } Gallery(card.users, Modifier.padding(Size10dp), accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index ba91a76684..3724ba5d65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -103,6 +103,7 @@ import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader +import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingCommunityInPost import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingHashtagsInPost import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation @@ -952,6 +953,7 @@ private fun RenderLongFormHeaderForThread( mainImageModifier = Modifier, loadedImageModifier = MaterialTheme.colorScheme.imageModifier, accountViewModel = accountViewModel, + onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel) }, onError = { DefaultImageHeader(note, accountViewModel) }, ) } ?: run { From 2ee1285ea16ae7742a4dbb434b042271c87fae07 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 19:16:28 -0400 Subject: [PATCH 056/185] Removes old follows in Contact lists. --- .../model/nip02FollowLists/FollowListState.kt | 3 - .../reqCommand/user/UserObservers.kt | 43 +++++- .../ui/screen/loggedIn/AccountViewModel.kt | 5 +- .../datasource/FilterUserProfileLists.kt | 2 + .../profile/gallery/QuickActionGallery.kt | 2 +- .../profile/hashtags/FollowedTagsTabHeader.kt | 4 +- .../profile/hashtags/TabFollowedTags.kt | 33 ++-- .../nip02FollowList/ContactListEvent.kt | 143 +----------------- .../nip02FollowList/tags/AddressFollowTag.kt | 25 --- 9 files changed, 67 insertions(+), 193 deletions(-) delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/AddressFollowTag.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt index 87eca3f752..e8d8c31822 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt @@ -124,9 +124,6 @@ class FollowListState( } else { ContactListEvent.createFromScratch( followUsers = listOf(ContactTag(user.pubkeyHex, user.bestRelayHint(), null)), - followTags = emptyList(), - followGeohashes = emptyList(), - followCommunities = emptyList(), relayUse = emptyMap(), signer = signer, onReady = onDone, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 9920cd4242..2848f1d833 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -45,6 +46,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.sample import java.math.BigDecimal @@ -259,7 +261,7 @@ fun observeUserFollowCount( @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @Composable -fun observeUserTagFollows( +fun observeUserTagFollowCount( user: User, accountViewModel: AccountViewModel, ): State { @@ -269,12 +271,15 @@ fun observeUserTagFollows( // Subscribe in the LocalCache for changes that arrive in the device val flow = remember(user) { - user + accountViewModel + .hashtagFollows(user) .flow() - .follows.stateFlow + .metadata.stateFlow .sample(1000) - .mapLatest { userState -> - userState.user.latestContactList?.countFollowTags() ?: 0 + .mapLatest { noteState -> + (noteState.note.event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.size ?: 0 + }.onStart { + emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.size ?: 0) }.distinctUntilChanged() .flowOn(Dispatchers.Default) } @@ -282,6 +287,34 @@ fun observeUserTagFollows( return flow.collectAsStateWithLifecycle(0) } +@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) +@Composable +fun observeUserTagFollows( + user: User, + accountViewModel: AccountViewModel, +): State> { + // Subscribe in the relay for changes in the metadata of this user. + UserFinderFilterAssemblerSubscription(user, accountViewModel) + + // Subscribe in the LocalCache for changes that arrive in the device + val flow = + remember(user) { + accountViewModel + .hashtagFollows(user) + .flow() + .metadata.stateFlow + .sample(1000) + .mapLatest { noteState -> + (noteState.note.event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.sorted() ?: emptyList() + }.onStart { + emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.sorted() ?: emptyList()) + }.distinctUntilChanged() + .flowOn(Dispatchers.Default) + } + + return flow.collectAsStateWithLifecycle(emptyList()) +} + @Composable fun observeUserBookmarks( user: User, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 9985fc7cd4..9919b0f6f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -113,6 +113,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent +import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent @@ -719,10 +720,12 @@ class AccountViewModel( viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) } } - fun removefromMediaGallery(note: Note) { + fun removeFromMediaGallery(note: Note) { viewModelScope.launch(Dispatchers.IO) { account.removeFromGallery(note) } } + fun hashtagFollows(user: User): Note = LocalCache.getOrCreateAddressableNote(HashtagListEvent.createAddress(user.pubkeyHex)) + fun addPrivateBookmark(note: Note) { viewModelScope.launch(Dispatchers.IO) { account.addBookmark(note, true) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt index 51258fad51..f3108d494f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent val UserProfileListKinds = @@ -34,6 +35,7 @@ val UserProfileListKinds = BookmarkListEvent.KIND, PeopleListEvent.KIND, FollowListEvent.KIND, + HashtagListEvent.KIND, AppRecommendationEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt index d32bf9456f..778e99afdd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/QuickActionGallery.kt @@ -64,7 +64,7 @@ fun DeleteFromGalleryDialog( buttonIcon = Icons.Default.Delete, buttonText = stringRes(R.string.quick_action_delete_dialog_btn), onClickDoOnce = { - accountViewModel.removefromMediaGallery(note) + accountViewModel.removeFromMediaGallery(note) onDismiss() }, onDismiss = onDismiss, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt index af082b3f0e..4c46aa32bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserTagFollows +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserTagFollowCount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -34,7 +34,7 @@ fun FollowedTagsTabHeader( baseUser: User, accountViewModel: AccountViewModel, ) { - val usertags by observeUserTagFollows(baseUser, accountViewModel) + val usertags by observeUserTagFollowCount(baseUser, accountViewModel) Text(text = "$usertags ${stringRes(R.string.followed_tags)}") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt index fa9c00e4ec..ee5cae3bc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt @@ -22,15 +22,17 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserTagFollows import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -43,28 +45,23 @@ fun TabFollowedTags( accountViewModel: AccountViewModel, nav: INav, ) { - val items = - remember(baseUser.latestContactList?.id) { - baseUser.latestContactList?.unverifiedFollowTagSet() - } - Column( Modifier .fillMaxHeight() .padding(vertical = 0.dp), ) { - items?.let { - LazyColumn { - itemsIndexed(items) { index, hashtag -> - HashtagHeader( - tag = hashtag, - account = accountViewModel, - onClick = { nav.nav(Route.Hashtag(hashtag)) }, - ) - HorizontalDivider( - thickness = DividerThickness, - ) - } + val items by observeUserTagFollows(baseUser, accountViewModel) + + LazyColumn(Modifier.fillMaxSize()) { + itemsIndexed(items) { index, hashtag -> + HashtagHeader( + tag = hashtag, + account = accountViewModel, + onClick = { nav.nav(Route.Hashtag(hashtag)) }, + ) + HorizontalDivider( + thickness = DividerThickness, + ) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt index 56f46bad2e..968ad43ab0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt @@ -30,16 +30,11 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint -import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHash -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.countHashtags import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHash import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip02FollowList.tags.AddressFollowTag import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -71,18 +66,16 @@ class ContactListEvent( /** * Returns a list of a-tags that are verified as correct. */ - fun verifiedFollowAddressSet(): Set = tags.mapNotNullTo(HashSet(), AddressFollowTag::parseValidAddress) + @Deprecated("Use CommunityListEvent instead.") + fun verifiedFollowAddressSet(): Set = tags.mapNotNullTo(HashSet(), ATag::parseValidAddress) fun unverifiedFollowKeySet() = tags.mapNotNull(ContactTag::parseKey) + @Deprecated("Use HashtagListEvent instead.") fun unverifiedFollowTagSet() = tags.hashtags() - fun countFollowTags() = tags.countHashtags() - fun follows() = tags.mapNotNull(ContactTag::parseValid) - fun followsTags() = hashtags() - fun relays(): Map? { val regular = RelaySet.parse(content) @@ -106,9 +99,6 @@ class ContactListEvent( fun createFromScratch( followUsers: List = emptyList(), - followTags: List = emptyList(), - followGeohashes: List = emptyList(), - followCommunities: List = emptyList(), relayUse: Map? = emptyMap(), signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), @@ -117,19 +107,13 @@ class ContactListEvent( val tags = listOf(AltTag.assemble(ALT)) + - followUsers.map { it.toTagArray() } + - followTags.map { arrayOf("t", it) } + - followCommunities.map { it.toATagArray() } + - followGeohashes.map { arrayOf("g", it) } + followUsers.map { it.toTagArray() } return signer.sign(createdAt, KIND, tags.toTypedArray(), content) } fun createFromScratch( followUsers: List, - followTags: List, - followGeohashes: List, - followCommunities: List, relayUse: Map?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), @@ -137,12 +121,7 @@ class ContactListEvent( ) { val content = relayUse?.let { RelaySet.assemble(it) } ?: "" - val tags = - followUsers.map { it.toTagArray() } + - followTags.map { arrayOf("t", it) } + - followCommunities.map { it.toATagArray() } + - followGeohashes.map { arrayOf("g", it) } - + val tags = followUsers.map { it.toTagArray() } return create( content = content, tags = tags.toTypedArray(), @@ -188,118 +167,6 @@ class ContactListEvent( ) } - fun followHashtag( - earlierVersion: ContactListEvent, - hashtag: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { - if (earlierVersion.isTaggedHash(hashtag)) return - - return create( - content = earlierVersion.content, - tags = earlierVersion.tags.plus(element = arrayOf("t", hashtag)), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - - fun unfollowHashtag( - earlierVersion: ContactListEvent, - hashtag: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { - if (!earlierVersion.isTaggedHash(hashtag)) return - - return create( - content = earlierVersion.content, - tags = - earlierVersion.tags.filter { it.size > 1 && !it[1].equals(hashtag, true) }.toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - - fun followGeohash( - earlierVersion: ContactListEvent, - hashtag: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { - if (earlierVersion.isTaggedGeoHash(hashtag)) return - - return create( - content = earlierVersion.content, - tags = earlierVersion.tags.plus(element = arrayOf("g", hashtag)), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - - fun unfollowGeohash( - earlierVersion: ContactListEvent, - hashtag: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { - if (!earlierVersion.isTaggedGeoHash(hashtag)) return - - return create( - content = earlierVersion.content, - tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag }.toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - - fun followAddressableEvent( - earlierVersion: ContactListEvent, - aTag: ATag, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { - if (earlierVersion.isTaggedAddressableNote(aTag.toTag())) return - - return create( - content = earlierVersion.content, - tags = - earlierVersion.tags.plus( - element = aTag.toATagArray(), - ), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - - fun unfollowAddressableEvent( - earlierVersion: ContactListEvent, - aTag: ATag, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { - if (!earlierVersion.isTaggedAddressableNote(aTag.toTag())) return - - return create( - content = earlierVersion.content, - tags = earlierVersion.tags.filter { it.size > 1 && it[1] != aTag.toTag() }.toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - fun updateRelayList( earlierVersion: ContactListEvent, relayUse: Map?, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/AddressFollowTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/AddressFollowTag.kt deleted file mode 100644 index 48d3cd43b0..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/tags/AddressFollowTag.kt +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip02FollowList.tags - -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag - -typealias AddressFollowTag = ATag From cc0280639059bf0314d58ebb525760a9e42a1422 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 8 Jul 2025 19:27:43 -0400 Subject: [PATCH 057/185] Fixing weird version number on the left drawer --- .../com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 21504cd17b..06ba28a751 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -785,7 +785,7 @@ fun BottomContent( nav.closeDrawer() }, ) { - append("v12" + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR.uppercase()) + append("v" + BuildConfig.VERSION_NAME + "-" + BuildConfig.FLAVOR.uppercase()) } } } From b12e675b3acf32123e1b12dd90e7eb03881ba3b3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 9 Jul 2025 08:36:50 -0400 Subject: [PATCH 058/185] Refactors the name of the simple relay client --- .../client/single/simple/{OutboxProtector.kt => OutboxCache.kt} | 2 +- .../nip01Core/relay/client/single/simple/SimpleRelayClient.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/{OutboxProtector.kt => OutboxCache.kt} (99%) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxCache.kt similarity index 99% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxCache.kt index 9e1884b9ab..7d6d581099 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxProtector.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/OutboxCache.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.utils.LargeCache -class OutboxProtector( +class OutboxCache( listener: IRelayClientListener, ) : RedirectRelayClientListener(listener) { /** diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt index 35d1bea227..6064dbc2a8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt @@ -40,7 +40,7 @@ class SimpleRelayClient( ) : IRelayClient by BasicRelayClient( url, socketBuilder, - OutboxProtector(listener), + OutboxCache(listener), stats, defaultOnConnect, ) From 6851b1bb5199f3d9897c3209ba81b65f6f0b0a3e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 9 Jul 2025 08:37:21 -0400 Subject: [PATCH 059/185] Only closes after removing the subscriptions to avoid resubscribing while closing. --- .../vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index 144c5273c3..c494a37d6c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -252,9 +252,9 @@ class NostrClient( } fun close(subscriptionId: String) { - relayPool.close(subscriptionId) activeRequests.remove(subscriptionId) activeCounts.remove(subscriptionId) + relayPool.close(subscriptionId) } override fun onEvent( From 17e33c61e7258eaf55746ada5ad1b88b37033834 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 9 Jul 2025 09:03:45 -0400 Subject: [PATCH 060/185] Refactoring base EOSE managers to provide better logging --- .../com/vitorpamplona/amethyst/DebugUtils.kt | 4 ++-- .../eoseManagers/BaseEoseManager.kt | 18 +++++++++++++++--- .../eoseManagers/PerUniqueIdEoseManager.kt | 6 +++--- .../PerUserAndFollowListEoseManager.kt | 7 ++++--- .../eoseManagers/PerUserEoseManager.kt | 6 +++--- .../eoseManagers/SingleSubEoseManager.kt | 2 +- .../SingleSubNoEoseCacheEoseManager.kt | 2 +- .../AccountGiftWrapsEoseManager.kt | 2 +- .../datasources/SubscriptionController.kt | 5 ++++- 9 files changed, 34 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 3b3301f601..ca70015c8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -141,10 +141,10 @@ fun debugState(context: Context) { LocalCache.addressables .sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 0L }) - qttNotes.toList().sortedByDescending { bytesNotes.get(it.first) }.forEach { (kind, qtt) -> + qttNotes.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) -> Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes.get(kind)?.div((1024 * 1024))}MB ") } - qttAddressables.toList().sortedByDescending { bytesNotes.get(it.first) }.forEach { (kind, qtt) -> + qttAddressables.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) -> Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables.get(kind)?.div((1024 * 1024))}MB ") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt index f90d643bd0..ac84ed46ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt @@ -25,17 +25,29 @@ import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.ammolite.relays.BundledUpdate import com.vitorpamplona.ammolite.relays.datasources.SubscriptionController import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.Dispatchers abstract class BaseEoseManager( val client: NostrClient, val allKeys: () -> Set, ) { - val orchestrator = SubscriptionController(client) + protected val logTag: String = this.javaClass.simpleName + + private val orchestrator = SubscriptionController(client) abstract fun updateSubscriptions(keys: Set) - fun printStats() = orchestrator.printStats(this.javaClass.simpleName) + fun printStats() = orchestrator.printStats(logTag) + + fun newSubscriptionId() = if (isDebug) logTag + newSubId() else newSubId() + + fun getSubscription(subId: String) = orchestrator.getSub(subId) + + fun requestNewSubscription(onEOSE: ((Long, NormalizedRelayUrl) -> Unit)? = null) = orchestrator.requestNewSubscription(newSubscriptionId(), onEOSE) + + fun dismissSubscription(subId: String) = orchestrator.dismissSubscription(subId) // Refreshes observers in batches. private val bundler = BundledUpdate(300, Dispatchers.Default) @@ -56,7 +68,7 @@ abstract class BaseEoseManager( bundler.cancel() orchestrator.destroy() if (isDebug) { - Log.d("${this.javaClass.simpleName}", "Destroy, Unsubscribe") + Log.d(logTag, "Destroy, Unsubscribe") } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt index 2a9f0169ba..045d6c9021 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -62,7 +62,7 @@ abstract class PerUniqueIdEoseManager( } open fun newSub(key: T): Subscription = - orchestrator.requestNewSubscription { time, relayUrl -> + requestNewSubscription { time, relayUrl -> newEose(key, relayUrl, time) } @@ -70,7 +70,7 @@ abstract class PerUniqueIdEoseManager( key: String, subId: String, ) { - orchestrator.dismissSubscription(subId) + dismissSubscription(subId) userSubscriptionMap.remove(key) } @@ -80,7 +80,7 @@ abstract class PerUniqueIdEoseManager( return if (subId == null) { newSub(key).also { userSubscriptionMap[id] = it.id } } else { - orchestrator.getSub(subId) ?: newSub(key).also { userSubscriptionMap[id] = it.id } + getSubscription(subId) ?: newSub(key).also { userSubscriptionMap[id] = it.id } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index 9480606821..26e1a72e6c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlin.collections.distinctBy /** * This query type creates a new relay subscription for every logged-in @@ -57,7 +58,7 @@ abstract class PerUserAndFollowListEoseManager( ) = latestEOSEs.newEose(user(key), list(key), relay, time) open fun newSub(key: T): Subscription = - orchestrator.requestNewSubscription { time, relayUrl -> + requestNewSubscription { time, relayUrl -> newEose(key, relayUrl, time) if (invalidateAfterEose) { invalidateFilters() @@ -68,7 +69,7 @@ abstract class PerUserAndFollowListEoseManager( key: User, subId: String, ) { - orchestrator.dismissSubscription(subId) + dismissSubscription(subId) userSubscriptionMap.remove(key) } @@ -78,7 +79,7 @@ abstract class PerUserAndFollowListEoseManager( return if (subId == null) { newSub(key).also { userSubscriptionMap[user] = it.id } } else { - orchestrator.getSub(subId) ?: newSub(key).also { userSubscriptionMap[user] = it.id } + getSubscription(subId) ?: newSub(key).also { userSubscriptionMap[user] = it.id } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index 404ab61431..e6e139c7be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -56,7 +56,7 @@ abstract class PerUserEoseManager( ) = latestEOSEs.newEose(user(key), relay, time) open fun newSub(key: T): Subscription = - orchestrator.requestNewSubscription { time, relayUrl -> + requestNewSubscription { time, relayUrl -> newEose(key, relayUrl, time) if (invalidateAfterEose) { invalidateFilters() @@ -67,7 +67,7 @@ abstract class PerUserEoseManager( key: User, subId: String, ) { - orchestrator.dismissSubscription(subId) + dismissSubscription(subId) userSubscriptionMap.remove(key) } @@ -77,7 +77,7 @@ abstract class PerUserEoseManager( return if (subId == null) { newSub(key).also { userSubscriptionMap[user] = it.id } } else { - orchestrator.getSub(subId) ?: newSub(key).also { userSubscriptionMap[user] = it.id } + getSubscription(subId) ?: newSub(key).also { userSubscriptionMap[user] = it.id } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt index c11aff6578..443f034aca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt @@ -56,7 +56,7 @@ abstract class SingleSubEoseManager( ) = latestEOSEs.newEose(relay, time) val sub = - orchestrator.requestNewSubscription { time, relayUrl -> + requestNewSubscription { time, relayUrl -> newEose(relayUrl, time) if (invalidateAfterEose) { invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt index 90d8d79fa5..bd03a71451 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt @@ -36,7 +36,7 @@ abstract class SingleSubNoEoseCacheEoseManager( val invalidateAfterEose: Boolean = false, ) : BaseEoseManager(client, allKeys) { val sub = - orchestrator.requestNewSubscription { time, relayUrl -> + requestNewSubscription { time, relayUrl -> if (invalidateAfterEose) { invalidateFilters() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt index 0622fac81d..4601b97ca7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt @@ -38,7 +38,7 @@ class AccountGiftWrapsEoseManager( client: NostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { - override fun user(query: AccountQueryState) = query.account.userProfile() + override fun user(key: AccountQueryState) = key.account.userProfile() override fun updateFilter( key: AccountQueryState, diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt index d1767f3fa4..b1a6f91695 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt +++ b/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt @@ -78,7 +78,10 @@ class SubscriptionController( fun getSub(subId: String) = subscriptions.get(subId) - fun requestNewSubscription(onEOSE: ((Long, NormalizedRelayUrl) -> Unit)? = null): Subscription = Subscription(onEose = onEOSE).also { subscriptions.put(it.id, it) } + fun requestNewSubscription( + subId: String, + onEOSE: ((Long, NormalizedRelayUrl) -> Unit)? = null, + ): Subscription = Subscription(subId, onEose = onEOSE).also { subscriptions.put(it.id, it) } fun dismissSubscription(subId: String) = getSub(subId)?.let { dismissSubscription(it) } From 7eb39b3c4c28c191c3ebf5b867d6499350513c16 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 9 Jul 2025 09:03:56 -0400 Subject: [PATCH 061/185] Readjusts relay view on profile --- .../loggedIn/profile/relays/RelayFeedView.kt | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt index 3076df88d6..d83373ab02 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt @@ -29,7 +29,9 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox +import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.note.RelayCompose @@ -48,28 +50,37 @@ fun RelayFeedView( RefresheableBox(viewModel, enablePullRefresh) { val listState = rememberLazyListState() - val clipboardManager = LocalClipboardManager.current LazyColumn( contentPadding = FeedPadding, state = listState, ) { - itemsIndexed(feedState, key = { _, item -> item.url }) { _, item -> - RelayCompose( - item, - accountViewModel = accountViewModel, - onAddRelay = { - clipboardManager.setText(AnnotatedString(item.url.url)) - nav.nav(Route.EditRelays) - }, - onRemoveRelay = { - nav.nav(Route.EditRelays) - }, - ) - HorizontalDivider( - thickness = DividerThickness, - ) + itemsIndexed(feedState, key = { _, item -> item.url.url }) { _, item -> + RenderRelayRow(item, accountViewModel, nav) } } } } + +@Composable +private fun RenderRelayRow( + relay: RelayInfo, + accountViewModel: AccountViewModel, + nav: INav, +) { + val clipboardManager = LocalClipboardManager.current + RelayCompose( + relay, + accountViewModel = accountViewModel, + onAddRelay = { + clipboardManager.setText(AnnotatedString(relay.url.url)) + nav.nav(Route.EditRelays) + }, + onRemoveRelay = { + nav.nav(Route.EditRelays) + }, + ) + HorizontalDivider( + thickness = DividerThickness, + ) +} From b502ab7de88633c427ae24f1513ff3494f68064a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 9 Jul 2025 15:08:11 -0400 Subject: [PATCH 062/185] Fixes issue on saving NIP47 address --- .../main/java/com/vitorpamplona/amethyst/LocalPreferences.kt | 2 +- .../quartz/nip47WalletConnect/Nip47WalletConnect.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index bd52ded9a5..414021289e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -324,7 +324,7 @@ object LocalPreferences { ) putString( PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, - EventMapper.mapper.writeValueAsString(settings.zapPaymentRequest), + EventMapper.mapper.writeValueAsString(settings.zapPaymentRequest?.denormalize()), ) if (settings.backupContactList != null) { putString( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt index ef4bdbe987..5a755f6d6a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt @@ -77,5 +77,7 @@ class Nip47WalletConnect { val pubKeyHex: HexKey, val relayUri: NormalizedRelayUrl, val secret: HexKey?, - ) + ) { + fun denormalize(): Nip47URI? = Nip47URI(pubKeyHex, relayUri.url, secret) + } } From 267d4c505a8e16e8d0920ea28dcfdef60bce61d1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 9 Jul 2025 15:14:58 -0400 Subject: [PATCH 063/185] Moves OkHttp to the coroutines version and turns all their usage into suspend functions --- amethyst/build.gradle | 1 + .../com/vitorpamplona/amethyst/CashuBTest.kt | 10 +- .../amethyst/DMFileDecryptionTest.kt | 1 - .../com/vitorpamplona/amethyst/DebugUtils.kt | 5 +- .../vitorpamplona/amethyst/model/Account.kt | 36 +- .../amethyst/model/LocalCache.kt | 5 + .../amethyst/service/CashuProcessor.kt | 426 ------------------ .../service/Nip05NostrAddressVerifier.kt | 36 +- .../service/Nip11RelayInfoRetriever.kt | 65 +-- .../amethyst/service/OnlineCheck.kt | 23 +- .../amethyst/service/ZapPaymentHandler.kt | 377 ++++++++-------- .../service/cashu/CachedCashuParser.kt | 40 ++ .../amethyst/service/cashu/CashuParser.kt | 43 ++ .../amethyst/service/cashu/CashuToken.kt | 41 ++ .../service/cashu/melt/MeltProcessor.kt | 230 ++++++++++ .../amethyst/service/cashu/melt/MeltResult.kt | 29 ++ .../amethyst/service/cashu/v3/V3Parser.kt | 63 +++ .../amethyst/service/cashu/v3/V3Token.kt | 37 ++ .../amethyst/service/cashu/v4/V4Models.kt | 68 +++ .../amethyst/service/cashu/v4/V4Parser.kt | 74 +++ .../service/lnurl/LightningAddressResolver.kt | 373 ++++++++------- .../service/notifications/RegisterAccounts.kt | 96 ++-- .../service/ots/OkHttpBitcoinExplorer.kt | 3 - .../amethyst/service/ots/OkHttpCalendar.kt | 3 - .../service/ots/OkHttpCalendarAsyncSubmit.kt | 2 - .../controls/RenderControlButtons.kt | 25 +- .../amethyst/service/previews/UrlPreview.kt | 39 +- .../uploads/blossom/BlossomUploader.kt | 56 +-- .../service/uploads/nip96/Nip96Uploader.kt | 105 ++--- .../uploads/nip96/ServerInfoRetriever.kt | 27 +- .../amethyst/ui/actions/EditPostView.kt | 7 +- .../amethyst/ui/actions/MediaSaverToDisk.kt | 96 ++-- .../amethyst/ui/components/CashuRedeem.kt | 8 +- .../ui/components/ZoomableContentDialog.kt | 4 +- .../amethyst/ui/note/ZapCustomDialog.kt | 39 +- .../note/creators/invoice/InvoiceRequest.kt | 15 +- .../creators/invoice/NewPostInvoiceRequest.kt | 22 +- .../nip22Comments/GenericCommentPostScreen.kt | 4 +- .../ui/screen/AccountStateViewModel.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 219 +++++---- .../nip99Classifieds/NewProductScreen.kt | 6 +- .../loggedIn/home/ShortNotePostScreen.kt | 4 +- .../profile/header/DisplayLNAddress.kt | 9 +- .../profile/header/DrawAdditionalInfo.kt | 16 +- gradle/libs.versions.toml | 3 +- .../quartz/nip57Zaps/LnZapRequestEvent.kt | 2 +- .../quartz/utils/ParallelUtils.kt | 74 ++- 47 files changed, 1552 insertions(+), 1317 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CachedCashuParser.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuToken.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltProcessor.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltResult.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Token.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Models.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 38147f0ca7..99f896e128 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -222,6 +222,7 @@ dependencies { // Websockets API implementation libs.okhttp + implementation libs.okhttpCoroutines // Encrypted Key Storage implementation libs.androidx.security.crypto.ktx diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt index ff697d6c82..200c896d79 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/CashuBTest.kt @@ -21,8 +21,8 @@ package com.vitorpamplona.amethyst import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.amethyst.service.CashuProcessor -import com.vitorpamplona.amethyst.service.CashuToken +import com.vitorpamplona.amethyst.service.cashu.CashuParser +import com.vitorpamplona.amethyst.service.cashu.CashuToken import com.vitorpamplona.amethyst.ui.components.GenericLoadable import junit.framework.TestCase.assertEquals import kotlinx.coroutines.runBlocking @@ -38,7 +38,7 @@ class CashuBTest { @Test() fun parseCashuA() { runBlocking { - val parsed = (CashuProcessor().parse(cashuTokenA) as GenericLoadable.Loaded>).loaded[0] + val parsed = (CashuParser().parse(cashuTokenA) as GenericLoadable.Loaded>).loaded[0] assertEquals(cashuTokenA, parsed.token) assertEquals("https://8333.space:3338", parsed.mint) @@ -59,7 +59,7 @@ class CashuBTest { @Test() fun parseCashuB() = runBlocking { - val parsed = (CashuProcessor().parse(cashuTokenB1) as GenericLoadable.Loaded>).loaded + val parsed = (CashuParser().parse(cashuTokenB1) as GenericLoadable.Loaded>).loaded assertEquals(cashuTokenB1, parsed[0].token) assertEquals("http://localhost:3338", parsed[0].mint) @@ -84,7 +84,7 @@ class CashuBTest { @Test() fun parseCashuB2() = runBlocking { - val parsed = (CashuProcessor().parse(cashuTokenB2) as GenericLoadable.Loaded>).loaded + val parsed = (CashuParser().parse(cashuTokenB2) as GenericLoadable.Loaded>).loaded assertEquals(cashuTokenB2, parsed[0].token) assertEquals("http://lbutlh5lfggq5r7xpiwhrajdl7sxpupgagazxl65w4c5cg72wtofasad.onion:3338", parsed[0].mint) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt index d00972ce2a..7e3e34a6ec 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/DMFileDecryptionTest.kt @@ -58,7 +58,6 @@ class DMFileDecryptionTest { val request = Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(url) .get() .build() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index ca70015c8e..6fd11fd166 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import kotlin.time.DurationUnit import kotlin.time.measureTimedValue +@Suppress("SENSELESS_COMPARISON") val isDebug = BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "benchmark" fun debugState(context: Context) { @@ -142,10 +143,10 @@ fun debugState(context: Context) { .sumByGroup(groupMap = { _, it -> it.event?.kind }, sumOf = { _, it -> it.event?.countMemory() ?: 0L }) qttNotes.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) -> - Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes.get(kind)?.div((1024 * 1024))}MB ") + Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesNotes[kind]?.div((1024 * 1024))}MB ") } qttAddressables.toList().sortedByDescending { bytesNotes[it.first] }.forEach { (kind, qtt) -> - Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables.get(kind)?.div((1024 * 1024))}MB ") + Log.d("STATE DUMP", "Kind ${kind.toString().padStart(5,' ')}:\t${qtt.toString().padStart(6,' ')} elements\t${bytesAddressables[kind]?.div((1024 * 1024))}MB ") } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 698198ed9b..0e388f09c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -167,6 +167,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessa import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits @@ -625,19 +626,23 @@ class Account( } fun createZapRequestFor( - userPubKeyHex: String, + user: User, message: String = "", zapType: LnZapEvent.ZapType, onReady: (LnZapRequestEvent) -> Unit, ) { + val relays = nip65RelayList.inboxFlow.value + user.inboxRelays() + LnZapRequestEvent.create( - userPubKeyHex, - nip65RelayList.inboxFlow.value.toSet(), - signer, - message, - zapType, - onReady = onReady, - ) + userHex = user.pubkeyHex, + relays = relays, + signer = signer, + message = message, + zapType = zapType, + ) { zapRequest -> + LocalCache.justConsumeMyOwnEvent(zapRequest) + onReady(zapRequest) + } } suspend fun report( @@ -1989,6 +1994,21 @@ class Account( } } + suspend fun decryptZapOrNull(event: LnZapRequestEvent): LnZapPrivateEvent? = + if (event.isPrivateZap()) { + if (isWriteable()) { + tryAndWait { continuation -> + event.decryptPrivateZap(signer) { + continuation.resume(it) + } + } + } else { + null + } + } else { + null + } + fun isAllHidden(users: Set): Boolean = users.all { isHidden(it) } fun isHidden(user: User) = isHidden(user.pubkeyHex) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 550c86fc8b..596f669237 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -628,6 +628,7 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) + @Suppress("DEPRECATION") fun consume( event: TorrentCommentEvent, relay: NormalizedRelayUrl?, @@ -682,6 +683,7 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) + @Suppress("DEPRECATION") fun consume( event: GitReplyEvent, relay: NormalizedRelayUrl?, @@ -778,6 +780,7 @@ object LocalCache : ILocalCache { return false } + @Suppress("DEPRECATION") fun computeReplyTo(event: Event): List = when (event) { is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } @@ -1318,6 +1321,7 @@ object LocalCache : ILocalCache { return false } + @Suppress("DEPRECATION") private fun deleteNote(deleteNote: Note) { val deletedEvent = deleteNote.event @@ -3057,6 +3061,7 @@ object LocalCache : ILocalCache { } } + @Suppress("DEPRECATION") private fun justConsumeInnerInner( event: Event, relay: NormalizedRelayUrl?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt deleted file mode 100644 index 08d8fdf5e6..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/CashuProcessor.kt +++ /dev/null @@ -1,426 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.service - -import android.content.Context -import android.util.LruCache -import androidx.compose.runtime.Immutable -import com.fasterxml.jackson.databind.node.JsonNodeFactory -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver -import com.vitorpamplona.amethyst.ui.components.GenericLoadable -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.cbor.ByteString -import kotlinx.serialization.cbor.Cbor -import kotlinx.serialization.decodeFromByteArray -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import java.util.Base64 -import kotlin.coroutines.cancellation.CancellationException - -@Immutable -data class CashuToken( - val token: String, - val mint: String, - val totalAmount: Long, - val proofs: List, -) - -@Serializable -@Immutable -class Proof( - val amount: Int, - val id: String, - val secret: String, - val C: String, -) - -object CachedCashuProcessor { - val cashuCache = LruCache>>(20) - - fun cached(token: String): GenericLoadable> = cashuCache[token] ?: GenericLoadable.Loading() - - fun parse(token: String): GenericLoadable> { - if (cashuCache[token] !is GenericLoadable.Loaded) { - checkNotInMainThread() - val newCachuData = CashuProcessor().parse(token) - - cashuCache.put(token, newCachuData) - } - - return cashuCache[token] - } -} - -class CashuProcessor { - @Serializable - class V3Token( - val unit: String?, - val memo: String?, - val token: List?, - ) - - @Serializable - class V3T( - val mint: String, - val proofs: List, - ) - - fun parse(cashuToken: String): GenericLoadable> { - checkNotInMainThread() - - if (cashuToken.startsWith("cashuA")) { - return parseCashuA(cashuToken) - } - - if (cashuToken.startsWith("cashuB")) { - return parseCashuB(cashuToken) - } - - return GenericLoadable.Error("Could not parse this cashu token") - } - - fun parseCashuA(cashuToken: String): GenericLoadable> { - checkNotInMainThread() - - try { - val base64token = cashuToken.replace("cashuA", "") - val cashu = jacksonObjectMapper().readValue(String(Base64.getDecoder().decode(base64token))) - - if (cashu.token == null) { - return GenericLoadable.Error("No token found") - } - - val converted = - cashu.token.map { token -> - val proofs = token.proofs - val mint = token.mint - - var totalAmount = 0L - for (proof in proofs) { - totalAmount += proof.amount - } - - CashuToken(cashuToken, mint, totalAmount, proofs) - } - - return GenericLoadable.Loaded(converted.toImmutableList()) - } catch (e: Exception) { - if (e is CancellationException) throw e - return GenericLoadable.Error("Could not parse this cashu token") - } - } - - @Serializable - class V4Token( - // mint - val m: String, - // unit - val u: String, - // memo - val d: String? = null, - val t: Array?, - ) - - @Serializable - class V4T( - // identifier - @ByteString - val i: ByteArray, - val p: Array, - ) - - @Serializable - class V4Proof( - // amount - val a: Int, - // secret - val s: String, - // signature - @ByteString - val c: ByteArray, - // no idea what this is - val d: V4DleqProof? = null, - // witness - val w: String? = null, - ) - - @Serializable - class V4DleqProof( - @ByteString - val e: ByteArray, - @ByteString - val s: ByteArray, - @ByteString - val r: ByteArray, - ) - - @OptIn(ExperimentalSerializationApi::class) - fun parseCashuB(cashuToken: String): GenericLoadable> { - checkNotInMainThread() - - try { - val base64token = cashuToken.replace("cashuB", "") - - val parser = Cbor { ignoreUnknownKeys = true } - - val v4Token = parser.decodeFromByteArray(Base64.getUrlDecoder().decode(base64token)) - - val v4proofs = v4Token.t ?: return GenericLoadable.Error("No token found") - - val converted = - v4proofs.map { id -> - val proofs = - id.p.map { - Proof( - it.a, - id.i.toHexKey(), - it.s, - it.c.toHexKey(), - ) - } - val mint = v4Token.m - - var totalAmount = 0L - for (proof in proofs) { - totalAmount += proof.amount - } - - CashuToken(cashuToken, mint, totalAmount, proofs) - } - - return GenericLoadable.Loaded(converted.toImmutableList()) - } catch (e: Exception) { - e.printStackTrace() - if (e is CancellationException) throw e - return GenericLoadable.Error("Could not parse this cashu token") - } - } - - suspend fun melt( - token: CashuToken, - lud16: String, - okHttpClient: (String) -> OkHttpClient, - onSuccess: (String, String) -> Unit, - onError: (String, String) -> Unit, - context: Context, - ) { - checkNotInMainThread() - - runCatching { - LightningAddressResolver() - .lnAddressInvoice( - lnaddress = lud16, - // Make invoice and leave room for fees - milliSats = token.totalAmount * 1000, - message = "Calculate Fees for Cashu", - okHttpClient = okHttpClient, - onSuccess = { baseInvoice -> - feeCalculator( - token.mint, - baseInvoice, - okHttpClient = okHttpClient, - onSuccess = { fees -> - LightningAddressResolver() - .lnAddressInvoice( - lnaddress = lud16, - // Make invoice and leave room for fees - milliSats = (token.totalAmount - fees) * 1000, - message = "Redeem Cashu", - okHttpClient = okHttpClient, - onSuccess = { invoice -> - meltInvoice(token, invoice, fees, okHttpClient, onSuccess, onError, context) - }, - onProgress = {}, - onError = onError, - context = context, - ) - }, - onError = onError, - context, - ) - }, - onProgress = {}, - onError = onError, - context = context, - ) - } - } - - fun feeCalculator( - mintAddress: String, - invoice: String, - okHttpClient: (String) -> OkHttpClient, - onSuccess: (Int) -> Unit, - onError: (String, String) -> Unit, - context: Context, - ) { - checkNotInMainThread() - - try { - val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint - val client = okHttpClient(url) - - val factory = JsonNodeFactory.instance - - val jsonObject = factory.objectNode() - jsonObject.put("pr", invoice) - - val mediaType = "application/json; charset=utf-8".toMediaType() - val requestBody = jsonObject.toString().toRequestBody(mediaType) - val request = - Request - .Builder() - .url(url) - .post(requestBody) - .build() - - client.newCall(request).execute().use { - val body = it.body.string() - val tree = jacksonObjectMapper().readTree(body) - - val feeCost = tree?.get("fee")?.asInt() - - if (feeCost != null) { - onSuccess( - feeCost, - ) - } else { - val msg = - tree - ?.get("detail") - ?.asText() - ?.split('.') - ?.getOrNull(0) - ?.ifBlank { null } - onError( - stringRes(context, R.string.cashu_failed_redemption), - if (msg != null) { - stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, msg) - } else { - stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg) - }, - ) - } - } - } catch (e: Exception) { - if (e is CancellationException) throw e - onError( - stringRes(context, R.string.cashu_successful_redemption), - stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message), - ) - } - } - - private fun meltInvoice( - token: CashuToken, - invoice: String, - fees: Int, - okHttpClient: (String) -> OkHttpClient, - onSuccess: (String, String) -> Unit, - onError: (String, String) -> Unit, - context: Context, - ) { - try { - val url = token.mint + "/melt" // Melt cashu tokens at Mint - val client = okHttpClient(url) - - val factory = JsonNodeFactory.instance - - val jsonObject = factory.objectNode() - - jsonObject.replace( - "proofs", - factory.arrayNode(token.proofs.size).apply { - token.proofs.forEach { - addObject().apply { - put("amount", it.amount) - put("id", it.id) - put("secret", it.secret) - put("C", it.C) - } - } - }, - ) - - jsonObject.put("pr", invoice) - - val mediaType = "application/json; charset=utf-8".toMediaType() - val requestBody = jsonObject.toString().toRequestBody(mediaType) - val request = - Request - .Builder() - .url(url) - .post(requestBody) - .build() - - client.newCall(request).execute().use { - val body = it.body.string() - val tree = jacksonObjectMapper().readTree(body) - - val successful = tree?.get("paid")?.asText() == "true" - - if (successful) { - onSuccess( - stringRes(context, R.string.cashu_successful_redemption), - stringRes( - context, - R.string.cashu_successful_redemption_explainer, - token.totalAmount.toString(), - fees.toString(), - ), - ) - } else { - val msg = - tree - ?.get("detail") - ?.asText() - ?.split('.') - ?.getOrNull(0) - ?.ifBlank { null } - onError( - stringRes(context, R.string.cashu_failed_redemption), - if (msg != null) { - stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, msg) - } else { - stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg) - }, - ) - } - } - } catch (e: Exception) { - if (e is CancellationException) throw e - onError( - stringRes(context, R.string.cashu_successful_redemption), - stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message), - ) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt index c3b0497a8b..35fb7b90dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip05NostrAddressVerifier.kt @@ -20,18 +20,18 @@ */ package com.vitorpamplona.amethyst.service -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.quartz.nip05DnsIdentifiers.Nip05 import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.coroutines.executeAsync class Nip05NostrAddressVerifier { suspend fun fetchNip05Json( nip05: String, - okttpClient: (String) -> OkHttpClient, + okHttpClient: (String) -> OkHttpClient, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit, ) = withContext(Dispatchers.IO) { @@ -45,30 +45,22 @@ class Nip05NostrAddressVerifier { } try { - val request = - Request - .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") - .url(url) - .build() - // Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint. - okttpClient(url) - .newBuilder() - .followRedirects(false) - .build() - .newCall(request) - .execute() - .use { - checkNotInMainThread() + val request = Request.Builder().url(url).build() - if (it.isSuccessful) { - onSuccess(it.body.string()) + // Fetchers MUST ignore any HTTP redirects given by the /.well-known/nostr.json endpoint. + val client = okHttpClient(url).newBuilder().followRedirects(false).build() + + client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (response.isSuccessful) { + onSuccess(response.body.string()) } else { onError( - "Could not resolve $nip05. Error: ${it.code}. Check if the server is up and if the address $nip05 is correct", + "Could not resolve $nip05. Error: ${response.code}. Check if the server is up and if the address $nip05 is correct", ) } } + } } catch (e: Exception) { if (e is CancellationException) throw e onError("Could not resolve NIP-05 $nip05 as URL $url: ${e.message}") @@ -77,7 +69,7 @@ class Nip05NostrAddressVerifier { suspend fun verifyNip05( nip05: String, - okttpClient: (String) -> OkHttpClient, + okHttpClient: (String) -> OkHttpClient, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit, ) { @@ -86,7 +78,7 @@ class Nip05NostrAddressVerifier { fetchNip05Json( nip05, - okttpClient, + okHttpClient, onSuccess = { checkNotInMainThread() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt index 5a23ad813a..3c33640931 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt @@ -22,18 +22,18 @@ package com.vitorpamplona.amethyst.service import android.util.Log import android.util.LruCache +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.CancellationException -import okhttp3.Call -import okhttp3.Callback +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.Response -import java.io.IOException +import okhttp3.coroutines.executeAsync object Nip11CachedRetriever { sealed class RetrieveResult( @@ -160,7 +160,6 @@ class Nip11Retriever { onInfo: (Nip11RelayInformation) -> Unit, onError: (NormalizedRelayUrl, ErrorCode, String?) -> Unit, ) { - checkNotInMainThread() val url = relay.toHttp() try { val request: Request = @@ -170,44 +169,28 @@ class Nip11Retriever { .url(url) .build() - okHttpClient(url) - .newCall(request) - .enqueue( - object : Callback { - override fun onResponse( - call: Call, - response: Response, - ) { - checkNotInMainThread() - response.use { - val body = it.body.string() - try { - if (it.isSuccessful) { - onInfo(Nip11RelayInformation.fromJson(body)) - } else { - onError(relay, ErrorCode.FAIL_WITH_HTTP_STATUS, it.code.toString()) - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e( - "RelayInfoFail", - "Resulting Message from Relay ${relay.url} in not parseable: $body", - e, - ) - onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, e.message) - } - } - } + val client = okHttpClient(url) - override fun onFailure( - call: Call, - e: IOException, - ) { - Log.e("RelayInfoFail", "${relay.url} unavailable", e) - onError(relay, ErrorCode.FAIL_TO_REACH_SERVER, e.message) + client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + val body = response.body.string() + try { + if (response.isSuccessful) { + onInfo(Nip11RelayInformation.fromJson(body)) + } else { + onError(relay, ErrorCode.FAIL_WITH_HTTP_STATUS, response.code.toString()) } - }, - ) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e( + "RelayInfoFail", + "Resulting Message from Relay ${relay.url} in not parseable: $body", + e, + ) + onError(relay, ErrorCode.FAIL_TO_PARSE_RESULT, e.message) + } + } + } } catch (e: Exception) { if (e is CancellationException) throw e Log.e("RelayInfoFail", "Invalid URL ${relay.url}", e) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt index 5136c0c2f9..04f21d2fdb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt @@ -23,12 +23,12 @@ package com.vitorpamplona.amethyst.service import android.util.Log import android.util.LruCache import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.quartz.utils.RandomInstance import okhttp3.EventListener import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request +import okhttp3.coroutines.executeAsync import okio.ByteString.Companion.toByteString import kotlin.coroutines.cancellation.CancellationException @@ -49,12 +49,10 @@ object OnlineChecker { return false } - fun isOnline( + suspend fun isOnline( url: String?, - okttpClient: (String) -> OkHttpClient, + okHttpClient: (String) -> OkHttpClient, ): Boolean { - checkNotInMainThread() - if (url.isNullOrBlank()) return false if ((checkOnlineCache.get(url)?.timeInMs ?: 0) > System.currentTimeMillis() - fiveMinutes) { return checkOnlineCache.get(url).online @@ -66,7 +64,6 @@ object OnlineChecker { val request = Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(url.replace("wss+livekit://", "wss://")) .header("Upgrade", "websocket") .header("Connection", "Upgrade") @@ -76,29 +73,23 @@ object OnlineChecker { .build() val client = - okttpClient(url) + okHttpClient(url) .newBuilder() .eventListener(EventListener.NONE) .protocols(listOf(Protocol.HTTP_1_1)) .build() - client.newCall(request).execute().use { - checkNotInMainThread() - it.isSuccessful - } + client.newCall(request).executeAsync().use { it.isSuccessful } } else { val request = Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(url) .get() .build() + val client = okHttpClient(url) - okttpClient(url).newCall(request).execute().use { - checkNotInMainThread() - it.isSuccessful - } + client.newCall(request).executeAsync().use { it.isSuccessful } } checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index 505fb75f99..c334861355 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -29,23 +29,25 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.splits.BaseZapSplitSetup +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.utils.collectSuccessfulOperations +import com.vitorpamplona.quartz.utils.collectSuccessfulOperationsReturning +import com.vitorpamplona.quartz.utils.mapNotNullAsync +import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient +import kotlin.coroutines.resume import kotlin.math.round class ZapPaymentHandler( @@ -53,12 +55,25 @@ class ZapPaymentHandler( ) { @Immutable data class Payable( - val info: BaseZapSplitSetup, - val user: User?, + val info: MyZapSplitSetup, val amountMilliSats: Long, val invoice: String, ) + data class UnverifiedZapSplitSetup( + val lnAddress: String?, + val weight: Double = 1.0, + val relay: NormalizedRelayUrl? = null, + val user: User? = null, + ) + + data class MyZapSplitSetup( + val lnAddress: String, + val weight: Double = 1.0, + val relay: NormalizedRelayUrl? = null, + val user: User? = null, + ) + suspend fun zap( note: Note, amountMilliSats: Long, @@ -75,87 +90,118 @@ class ZapPaymentHandler( val noteEvent = note.event val zapSplitSetup = noteEvent?.zapSplitSetup() - val zapsToSend = + val unverifiedZapsToSend = if (!zapSplitSetup.isNullOrEmpty()) { - zapSplitSetup + zapSplitSetup.map { setup -> + when (setup) { + is ZapSplitSetupLnAddress -> { + UnverifiedZapSplitSetup( + lnAddress = setup.lnAddress, + weight = setup.weight, + ) + } + is ZapSplitSetup -> { + val user = LocalCache.checkGetOrCreateUser(setup.pubKeyHex) + UnverifiedZapSplitSetup( + lnAddress = user?.info?.lnAddress(), + weight = setup.weight, + relay = setup.relay, + user = user, + ) + } + } + } } else if (noteEvent is LiveActivitiesEvent && noteEvent.hasHost()) { - noteEvent.hosts().map { ZapSplitSetup(it.pubKey, it.relayHint, weight = 1.0) } + noteEvent.hosts().map { + val user = LocalCache.checkGetOrCreateUser(it.pubKey) + val lnAddress = user?.info?.lnAddress() + UnverifiedZapSplitSetup(lnAddress, relay = it.relayHint, user = user) + } } else if (noteEvent is AppDefinitionEvent) { val appLud16 = noteEvent.appMetaData()?.lnAddress() if (appLud16 != null) { - listOf(ZapSplitSetupLnAddress(appLud16, weight = 1.0)) + listOf(UnverifiedZapSplitSetup(appLud16)) } else { val lud16 = note.author?.info?.lnAddress() - - if (lud16.isNullOrBlank()) { - if (showErrorIfNoLnAddress) { - onError( - stringRes(context, R.string.missing_lud16), - stringRes( - context, - R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats, - ), - note.author, - ) - } - return@withContext - } - - listOf(ZapSplitSetupLnAddress(lud16, weight = 1.0)) + listOf(UnverifiedZapSplitSetup(lud16)) } } else { - val lud16 = note.author?.info?.lnAddress() + listOf(UnverifiedZapSplitSetup(note.author?.info?.lnAddress())) + } - if (lud16.isNullOrBlank()) { - if (showErrorIfNoLnAddress) { - onError( - stringRes(context, R.string.missing_lud16), - stringRes( - context, - R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats, - ), - note.author, + if (showErrorIfNoLnAddress) { + val errors = unverifiedZapsToSend.filter { it.lnAddress.isNullOrBlank() } + errors.forEach { + val message = + if (it.user != null) { + stringRes( + context, + R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, + it.user.toBestDisplayName(), ) + } else { + stringRes(context, R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats) } - return@withContext - } - listOf(ZapSplitSetupLnAddress(lud16, weight = 1.0)) + onError( + stringRes(context, R.string.missing_lud16), + message, + it.user, + ) + } + } + + val zapsToSend = + unverifiedZapsToSend.mapNotNull { + if (it.lnAddress != null) { + MyZapSplitSetup( + it.lnAddress, + it.weight, + it.relay, + it.user, + ) + } else { + null + } } onProgress(0.02f) - signAllZapRequests(note, pollOption, message, zapType, zapsToSend) { splitZapRequestPairs -> - if (splitZapRequestPairs.isEmpty()) { - onProgress(0.00f) - return@signAllZapRequests - } else { - onProgress(0.05f) - } - assembleAllInvoices(splitZapRequestPairs, amountMilliSats, message, showErrorIfNoLnAddress, okHttpClient, onError, onProgress = { - onProgress(it * 0.7f + 0.05f) // keeps within range. - }, context) { payables -> - if (payables.isEmpty()) { - onProgress(0.00f) - return@assembleAllInvoices - } else { - onProgress(0.75f) - } + val splitZapRequests = signAllZapRequests(note, pollOption, message, zapType, zapsToSend) - if (account.hasWalletConnectSetup()) { - payViaNWC(payables, note, onError = onError, onProgress = { - onProgress(it * 0.25f + 0.75f) // keeps within range. - }, context) { - // onProgress(1f) - } - } else { - onPayViaIntent( - payables.toImmutableList(), - ) + if (splitZapRequests.isEmpty()) { + onProgress(0.00f) + return@withContext + } else { + onProgress(0.05f) + } - onProgress(0f) - } - } + val payables = + assembleAllInvoices( + requests = splitZapRequests, + totalAmountMilliSats = amountMilliSats, + message = message, + okHttpClient = okHttpClient, + onError = onError, + onProgress = { onProgress(it * 0.7f + 0.05f) }, + context = context, + ) + + if (payables.isEmpty()) { + onProgress(0.00f) + return@withContext + } else { + onProgress(0.75f) + } + + if (account.hasWalletConnectSetup()) { + payViaNWC(payables, note, onError = onError, onProgress = { + onProgress(it * 0.25f + 0.75f) // keeps within range. + }, context) + // onProgress(1f) + } else { + onPayViaIntent(payables.toImmutableList()) + onProgress(0f) } } @@ -170,88 +216,76 @@ class ZapPaymentHandler( } class ZapRequestReady( - val inputSetup: BaseZapSplitSetup, - val zapRequestJson: String?, - val user: User? = null, + val inputSetup: MyZapSplitSetup, + val zapRequest: LnZapRequestEvent?, ) - fun receivingRelaySet(userHex: HexKey): Set? = - ( - LocalCache - .getAddressableNoteIfExists( - AdvertisedRelayListEvent.createAddressTag(userHex), - )?.event as? AdvertisedRelayListEvent - )?.readRelaysNorm() - ?.toSet() - suspend fun signAllZapRequests( note: Note, pollOption: Int?, message: String, zapType: LnZapEvent.ZapType, - zapsToSend: List, - onAllDone: suspend (List) -> Unit, - ) { - collectSuccessfulOperations( - items = zapsToSend, - runRequestFor = { next: BaseZapSplitSetup, onReady -> - if (next is ZapSplitSetupLnAddress) { - prepareZapRequestIfNeeded(note, pollOption, message, zapType) { zapRequestJson -> - if (zapRequestJson != null) { - onReady(ZapRequestReady(next, zapRequestJson)) - } - } - } else if (next is ZapSplitSetup) { - val authorRelayList = note.author?.let { receivingRelaySet(it.pubkeyHex) } ?: emptySet() - val userRelayList = receivingRelaySet(next.pubKeyHex) ?: emptySet() + zapsToSend: List, + ): List = + mapNotNullAsync(zapsToSend) { next: MyZapSplitSetup -> + // makes sure the author receives the zap event + val authorRelayList = note.author?.inboxRelays()?.toSet() ?: emptySet() - val user = LocalCache.getOrCreateUser(next.pubKeyHex) + // makes sure the zap split user receives the zap event + val userRelayList = next.user?.inboxRelays()?.toSet() ?: emptySet() - prepareZapRequestIfNeeded(note, pollOption, message, zapType, user, userRelayList + authorRelayList) { zapRequestJson -> - onReady(ZapRequestReady(next, zapRequestJson, user)) - } - } - }, - onReady = onAllDone, - ) - } + val zapRequest = prepareZapRequestIfNeeded(note, pollOption, message, zapType, next.user, userRelayList + authorRelayList) + + ZapRequestReady(next, zapRequest) + } suspend fun assembleAllInvoices( requests: List, totalAmountMilliSats: Long, message: String, - showErrorIfNoLnAddress: Boolean, okHttpClient: (String) -> OkHttpClient, onError: (String, String, User?) -> Unit, onProgress: (percent: Float) -> Unit, context: Context, - onAllDone: suspend (List) -> Unit, - ) { + ): List { var progressAllPayments = 0.00f val totalWeight = requests.sumOf { it.inputSetup.weight } - collectSuccessfulOperations( - items = requests, - runRequestFor = { splitZapRequestPair: ZapRequestReady, onReady -> + return mapNotNullAsync(requests) { splitZapRequestPair: ZapRequestReady -> + try { assembleInvoice( + lud16 = splitZapRequestPair.inputSetup.lnAddress, splitSetup = splitZapRequestPair.inputSetup, - nostrZapRequest = splitZapRequestPair.zapRequestJson, - toUser = splitZapRequestPair.user, + nostrZapRequest = splitZapRequestPair.zapRequest, zapValue = calculateZapValue(totalAmountMilliSats, splitZapRequestPair.inputSetup.weight, totalWeight), message = message, - showErrorIfNoLnAddress = showErrorIfNoLnAddress, okHttpClient = okHttpClient, - onError = onError, onProgressStep = { percentStepForThisPayment -> progressAllPayments += percentStepForThisPayment / requests.size onProgress(progressAllPayments) }, context = context, - onReady = onReady, ) - }, - onReady = onAllDone, - ) + } catch (e: LightningAddressResolver.LightningAddressError) { + onError(e.title, e.msg, splitZapRequestPair.inputSetup.user) + null + } catch (e: Exception) { + if (e is CancellationException) throw e + onError( + stringRes( + context, + R.string.error_unable_to_fetch_invoice, + ), + stringRes( + context, + R.string.unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error, + e.message, + ), + null, + ) + null + } + } } class Paid( @@ -265,11 +299,10 @@ class ZapPaymentHandler( onError: (String, String, User?) -> Unit, onProgress: (percent: Float) -> Unit, context: Context, - onAllDone: suspend (List) -> Unit, - ) { + ): List { var progressAllPayments = 0.00f - collectSuccessfulOperations( + return collectSuccessfulOperationsReturning( items = payables, runRequestFor = { payable: Payable, onReady -> account.sendZapPaymentRequestFor( @@ -292,7 +325,7 @@ class ZapPaymentHandler( response.error?.message ?: response.error?.code?.toString() ?: "Error parsing error message", ), - payable.user, + payable.info.user, ) } else { progressAllPayments += 0.5f / payables.size @@ -301,94 +334,60 @@ class ZapPaymentHandler( }, ) }, - onReady = onAllDone, ) } - private fun assembleInvoice( - splitSetup: BaseZapSplitSetup, - nostrZapRequest: String?, - toUser: User?, + private suspend fun assembleInvoice( + lud16: String, + splitSetup: MyZapSplitSetup, + nostrZapRequest: LnZapRequestEvent?, zapValue: Long, message: String, - showErrorIfNoLnAddress: Boolean = true, okHttpClient: (String) -> OkHttpClient, - onError: (String, String, User?) -> Unit, onProgressStep: (percent: Float) -> Unit, context: Context, - onReady: (Payable) -> Unit, - ) { + ): Payable { var progressThisPayment = 0.00f - val lud16 = - if (splitSetup is ZapSplitSetupLnAddress) { - splitSetup.lnAddress - } else { - toUser?.info?.lnAddress() - } + val invoice = + LightningAddressResolver().lnAddressInvoice( + lnAddress = lud16, + milliSats = zapValue, + message = message, + nostrRequest = nostrZapRequest, + okHttpClient = okHttpClient, + onProgress = { + val step = it - progressThisPayment + progressThisPayment = it + onProgressStep(step) + }, + context = context, + ) - if (lud16 != null) { - LightningAddressResolver() - .lnAddressInvoice( - lnaddress = lud16, - milliSats = zapValue, - message = message, - nostrRequest = nostrZapRequest, - okHttpClient = okHttpClient, - onError = { title, msg -> - onError(title, msg, toUser) - }, - onProgress = { - val step = it - progressThisPayment - progressThisPayment = it - onProgressStep(step) - }, - context = context, - onSuccess = { - onProgressStep(1 - progressThisPayment) - onReady( - Payable( - info = splitSetup, - user = toUser, - amountMilliSats = zapValue, - invoice = it, - ), - ) - }, - ) - } else { - if (showErrorIfNoLnAddress) { - onError( - stringRes( - context, - R.string.missing_lud16, - ), - stringRes( - context, - R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, - toUser?.toBestDisplayName() ?: splitSetup.mainId(), - ), - null, - ) - } - } + onProgressStep(1 - progressThisPayment) + + return Payable( + info = splitSetup, + amountMilliSats = zapValue, + invoice = invoice, + ) } - private fun prepareZapRequestIfNeeded( + private suspend fun prepareZapRequestIfNeeded( note: Note, pollOption: Int?, message: String, zapType: LnZapEvent.ZapType, overrideUser: User? = null, additionalRelays: Set? = null, - onReady: (String?) -> Unit, - ) { + ): LnZapRequestEvent? = if (zapType != LnZapEvent.ZapType.NONZAP) { - account.createZapRequestFor(note, pollOption, message, zapType, overrideUser, additionalRelays) { zapRequest -> - onReady(zapRequest.toJson()) + tryAndWait { continuation -> + account.createZapRequestFor(note, pollOption, message, zapType, overrideUser, additionalRelays) { zapRequest -> + continuation.resume(zapRequest) + } } } else { - onReady(null) + null } - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CachedCashuParser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CachedCashuParser.kt new file mode 100644 index 0000000000..c5a48dcc7c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CachedCashuParser.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.cashu + +import android.util.LruCache +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import kotlinx.collections.immutable.ImmutableList + +object CachedCashuParser { + val cashuCache = LruCache>>(20) + + fun cached(token: String): GenericLoadable> = cashuCache[token] ?: GenericLoadable.Loading() + + fun parse(token: String): GenericLoadable> { + if (cashuCache[token] !is GenericLoadable.Loaded) { + val newCachuData = CashuParser().parse(token) + cashuCache.put(token, newCachuData) + } + + return cashuCache[token] + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt new file mode 100644 index 0000000000..49101b4b4a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuParser.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.cashu + +import com.vitorpamplona.amethyst.service.cashu.v3.V3Parser +import com.vitorpamplona.amethyst.service.cashu.v4.V4Parser +import com.vitorpamplona.amethyst.service.checkNotInMainThread +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import kotlinx.collections.immutable.ImmutableList + +class CashuParser { + fun parse(cashuToken: String): GenericLoadable> { + checkNotInMainThread() + + if (cashuToken.startsWith("cashuA")) { + return V3Parser.parseCashuA(cashuToken) + } + + if (cashuToken.startsWith("cashuB")) { + return V4Parser.parseCashuB(cashuToken) + } + + return GenericLoadable.Error("Could not parse this cashu token") + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuToken.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuToken.kt new file mode 100644 index 0000000000..6c0eeb6d69 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/CashuToken.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.cashu + +import androidx.compose.runtime.Immutable +import kotlinx.serialization.Serializable + +@Immutable +data class CashuToken( + val token: String, + val mint: String, + val totalAmount: Long, + val proofs: List, +) + +@Serializable +@Immutable +class Proof( + val amount: Int, + val id: String, + val secret: String, + val C: String, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltProcessor.kt new file mode 100644 index 0000000000..c08ec63089 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltProcessor.kt @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.cashu.melt + +import android.content.Context +import com.fasterxml.jackson.databind.node.JsonNodeFactory +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.cashu.CashuToken +import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver +import com.vitorpamplona.amethyst.ui.stringRes +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.coroutines.executeAsync +import kotlin.coroutines.cancellation.CancellationException + +class MeltProcessor { + suspend fun melt( + token: CashuToken, + lud16: String, + okHttpClient: (String) -> OkHttpClient, + context: Context, + ): MeltResult { + val baseInvoice = + LightningAddressResolver().lnAddressInvoice( + lnAddress = lud16, + // Make invoice and leave room for fees + milliSats = token.totalAmount * 1000, + message = "Calculate Fees for Cashu", + okHttpClient = okHttpClient, + onProgress = {}, + context = context, + ) + + val fees = + feeCalculator( + mintAddress = token.mint, + invoice = baseInvoice, + okHttpClient = okHttpClient, + context = context, + ) + + val invoice = + LightningAddressResolver().lnAddressInvoice( + lnAddress = lud16, + // Make invoice and leave room for fees + milliSats = (token.totalAmount - fees) * 1000, + message = "Redeem Cashu", + okHttpClient = okHttpClient, + onProgress = {}, + context = context, + ) + + meltInvoice(token, invoice, okHttpClient, context) + + return MeltResult( + token = token, + invoice = invoice, + fees = fees, + ) + } + + suspend fun melt( + token: CashuToken, + lud16: String, + okHttpClient: (String) -> OkHttpClient, + onSuccess: (String, String) -> Unit, + onError: (String, String) -> Unit, + context: Context, + ) { + } + + suspend fun feeCalculator( + mintAddress: String, + invoice: String, + okHttpClient: (String) -> OkHttpClient, + context: Context, + ): Int = + try { + val url = "$mintAddress/checkfees" // Melt cashu tokens at Mint + val client = okHttpClient(url) + + val factory = JsonNodeFactory.instance + + val jsonObject = factory.objectNode() + jsonObject.put("pr", invoice) + + val mediaType = "application/json; charset=utf-8".toMediaType() + val requestBody = jsonObject.toString().toRequestBody(mediaType) + val request = + Request + .Builder() + .url(url) + .post(requestBody) + .build() + + client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + val body = response.body.string() + val tree = jacksonObjectMapper().readTree(body) + + val feeCost = tree?.get("fee")?.asInt() + + if (feeCost == null) { + val msg = + tree + ?.get("detail") + ?.asText() + ?.split('.') + ?.getOrNull(0) + ?.ifBlank { null } + + throw LightningAddressResolver.LightningAddressError( + stringRes(context, R.string.cashu_failed_redemption), + if (msg != null) { + stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, msg) + } else { + stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg) + }, + ) + } + + feeCost + } + } + } catch (e: Exception) { + if (e is CancellationException) throw e + throw LightningAddressResolver.LightningAddressError( + stringRes(context, R.string.cashu_failed_redemption), + stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message), + ) + } + + private suspend fun meltInvoice( + token: CashuToken, + invoice: String, + okHttpClient: (String) -> OkHttpClient, + context: Context, + ) { + try { + val url = token.mint + "/melt" // Melt cashu tokens at Mint + val client = okHttpClient(url) + + val factory = JsonNodeFactory.instance + + val jsonObject = factory.objectNode() + + jsonObject.replace( + "proofs", + factory.arrayNode(token.proofs.size).apply { + token.proofs.forEach { + addObject().apply { + put("amount", it.amount) + put("id", it.id) + put("secret", it.secret) + put("C", it.C) + } + } + }, + ) + + jsonObject.put("pr", invoice) + + val mediaType = "application/json; charset=utf-8".toMediaType() + val requestBody = jsonObject.toString().toRequestBody(mediaType) + val request = + Request + .Builder() + .url(url) + .post(requestBody) + .build() + + client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + val body = response.body.string() + val tree = jacksonObjectMapper().readTree(body) + + val successful = tree?.get("paid")?.asText() == "true" + + if (!successful) { + val msg = + tree + ?.get("detail") + ?.asText() + ?.split('.') + ?.getOrNull(0) + ?.ifBlank { null } + + throw LightningAddressResolver.LightningAddressError( + stringRes(context, R.string.cashu_failed_redemption), + if (msg != null) { + stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, msg) + } else { + stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg) + }, + ) + } + } + } + } catch (e: Exception) { + if (e is CancellationException) throw e + throw LightningAddressResolver.LightningAddressError( + stringRes(context, R.string.cashu_successful_redemption), + stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message), + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltResult.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltResult.kt new file mode 100644 index 0000000000..11ad4711ad --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/melt/MeltResult.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.cashu.melt + +import com.vitorpamplona.amethyst.service.cashu.CashuToken + +class MeltResult( + val token: CashuToken, + val invoice: String, + val fees: Int, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt new file mode 100644 index 0000000000..9650f2358f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.cashu.v3 + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.amethyst.service.cashu.CashuToken +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import java.util.Base64 +import kotlin.coroutines.cancellation.CancellationException + +class V3Parser { + companion object Companion { + fun parseCashuA(cashuToken: String): GenericLoadable> { + try { + val base64token = cashuToken.replace("cashuA", "") + val cashu = jacksonObjectMapper().readValue(String(Base64.getDecoder().decode(base64token))) + + if (cashu.token == null) { + return GenericLoadable.Error("No token found") + } + + val converted = + cashu.token.map { token -> + val proofs = token.proofs + val mint = token.mint + + var totalAmount = 0L + for (proof in proofs) { + totalAmount += proof.amount + } + + CashuToken(cashuToken, mint, totalAmount, proofs) + } + + return GenericLoadable.Loaded(converted.toImmutableList()) + } catch (e: Exception) { + if (e is CancellationException) throw e + return GenericLoadable.Error("Could not parse this cashu token") + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Token.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Token.kt new file mode 100644 index 0000000000..be244b2eda --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Token.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.cashu.v3 + +import com.vitorpamplona.amethyst.service.cashu.Proof +import kotlinx.serialization.Serializable + +@Serializable +class V3Token( + val unit: String?, + val memo: String?, + val token: List?, +) + +@Serializable +class V3T( + val mint: String, + val proofs: List, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Models.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Models.kt new file mode 100644 index 0000000000..071b4c7d7e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Models.kt @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.cashu.v4 + +import kotlinx.serialization.Serializable +import kotlinx.serialization.cbor.ByteString + +@Serializable +class V4Token( + // mint + val m: String, + // unit + val u: String, + // memo + val d: String? = null, + val t: Array?, +) + +@Serializable +class V4T( + // identifier + @ByteString + val i: ByteArray, + val p: Array, +) + +@Serializable +class V4Proof( + // amount + val a: Int, + // secret + val s: String, + // signature + @ByteString + val c: ByteArray, + // no idea what this is + val d: V4DleqProof? = null, + // witness + val w: String? = null, +) + +@Serializable +class V4DleqProof( + @ByteString + val e: ByteArray, + @ByteString + val s: ByteArray, + @ByteString + val r: ByteArray, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt new file mode 100644 index 0000000000..ddd22fb5e3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.cashu.v4 + +import com.vitorpamplona.amethyst.service.cashu.CashuToken +import com.vitorpamplona.amethyst.service.cashu.Proof +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.decodeFromByteArray +import java.util.Base64 +import kotlin.coroutines.cancellation.CancellationException + +class V4Parser { + companion object { + @OptIn(ExperimentalSerializationApi::class) + fun parseCashuB(cashuToken: String): GenericLoadable> { + try { + val base64token = cashuToken.replace("cashuB", "") + val parser = Cbor { ignoreUnknownKeys = true } + val v4Token = parser.decodeFromByteArray(Base64.getUrlDecoder().decode(base64token)) + val v4proofs = v4Token.t ?: return GenericLoadable.Error("No token found") + + val converted = + v4proofs.map { id -> + val proofs = + id.p.map { + Proof( + it.a, + id.i.toHexKey(), + it.s, + it.c.toHexKey(), + ) + } + val mint = v4Token.m + + var totalAmount = 0L + for (proof in proofs) { + totalAmount += proof.amount + } + + CashuToken(cashuToken, mint, totalAmount, proofs) + } + + return GenericLoadable.Loaded(converted.toImmutableList()) + } catch (e: Exception) { + e.printStackTrace() + if (e is CancellationException) throw e + return GenericLoadable.Error("Could not parse this cashu token") + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index c21c791342..42cc045fa0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -23,142 +23,137 @@ package com.vitorpamplona.amethyst.service.lnurl import android.content.Context import android.util.Log import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.lightning.Lud06 +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response +import okhttp3.coroutines.executeAsync import java.math.BigDecimal import java.math.RoundingMode import java.net.URLEncoder import kotlin.coroutines.cancellation.CancellationException class LightningAddressResolver { - fun assembleUrl(lnaddress: String): String? { - val parts = lnaddress.split("@") + fun assembleUrl(lnAddress: String): String? { + val parts = lnAddress.split("@") if (parts.size == 2) { return "https://${parts[1]}/.well-known/lnurlp/${parts[0]}" } - if (lnaddress.lowercase().startsWith("lnurl")) { - return Lud06().toLnUrlp(lnaddress) + if (lnAddress.lowercase().startsWith("lnurl")) { + return Lud06().toLnUrlp(lnAddress) } return null } - private fun fetchLightningAddressJson( - lnaddress: String, - okttpClient: (String) -> OkHttpClient, - onSuccess: (String) -> Unit, - onError: (String, String) -> Unit, - context: Context, - ) { - checkNotInMainThread() + class LightningAddressError( + val title: String, + val msg: String, + ) : Exception(msg) - val url = assembleUrl(lnaddress) + private suspend fun fetchLightningAddressJson( + lnAddress: String, + okHttpClient: (String) -> OkHttpClient, + context: Context, + ): String { + val url = assembleUrl(lnAddress) if (url == null) { - onError( + throw LightningAddressError( stringRes(context, R.string.error_unable_to_fetch_invoice), - stringRes( - context, - R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup, - lnaddress, - ), + stringRes(context, R.string.could_not_assemble_lnurl_from_lightning_address_check_the_user_s_setup, lnAddress), ) - return } - val client = okttpClient(url) + val client = okHttpClient(url) - try { + return try { val request: Request = Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(url) .build() - client.newCall(request).execute().use { - if (it.isSuccessful) { - onSuccess(it.body.string()) - } else { - onError( - stringRes(context, R.string.error_unable_to_fetch_invoice), - stringRes( - context, - R.string - .the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct, - url, - lnaddress, - errorMessage(it, context), - ), - ) + client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (response.isSuccessful) { + response.body.string() + } else { + throw LightningAddressError( + stringRes(context, R.string.error_unable_to_fetch_invoice), + stringRes( + context, + R.string + .the_receiver_s_lightning_service_at_is_not_available_it_was_calculated_from_the_lightning_address_error_check_if_the_server_is_up_and_if_the_lightning_address_is_correct, + url, + lnAddress, + errorMessage(response, context), + ), + ) + } } } } catch (e: Exception) { if (e is CancellationException) throw e - e.printStackTrace() - onError( + throw LightningAddressError( stringRes(context, R.string.error_unable_to_fetch_invoice), stringRes( context, R.string .could_not_resolve_check_if_you_are_connected_if_the_server_is_up_and_if_the_lightning_address_is_correct_exception, url, - lnaddress, + lnAddress, e.suppressedExceptions.getOrNull(0)?.message ?: e.cause?.message ?: e.message, ), ) } } - fun fetchLightningInvoice( + suspend fun fetchLightningInvoice( lnCallback: String, milliSats: Long, message: String, - nostrRequest: String? = null, - okttpClient: (String) -> OkHttpClient, - onSuccess: (String) -> Unit, - onError: (String, String) -> Unit, + nostrRequest: LnZapRequestEvent? = null, + okHttpClient: (String) -> OkHttpClient, context: Context, - ) { - checkNotInMainThread() - + ): String { val encodedMessage = URLEncoder.encode(message, "utf-8") val urlBinder = if (lnCallback.contains("?")) "&" else "?" var url = "$lnCallback${urlBinder}amount=$milliSats&comment=$encodedMessage" if (nostrRequest != null) { - val encodedNostrRequest = URLEncoder.encode(nostrRequest, "utf-8") + val encodedNostrRequest = URLEncoder.encode(nostrRequest.toJson(), "utf-8") url += "&nostr=$encodedNostrRequest" } - val client = okttpClient(url) + val client = okHttpClient(url) val request: Request = Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(url) .build() - client.newCall(request).execute().use { - if (it.isSuccessful) { - onSuccess(it.body.string()) - } else { - onError( - stringRes(context, R.string.error_unable_to_fetch_invoice), - stringRes(context, R.string.could_not_fetch_invoice_from_details, lnCallback, errorMessage(it, context)), - ) + return client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (response.isSuccessful) { + response.body.string() + } else { + throw LightningAddressError( + stringRes(context, R.string.error_unable_to_fetch_invoice), + stringRes(context, R.string.could_not_fetch_invoice_from_details, lnCallback, errorMessage(response, context)), + ) + } } } } @@ -178,7 +173,7 @@ class LightningAddressResolver { val statusNode = tree.get("status") if (tree.get("error").isBoolean && messageNode != null) { - if (errorNode.asBoolean() == true) { + if (errorNode.asBoolean()) { return messageNode.asText() } } @@ -203,146 +198,136 @@ class LightningAddressResolver { ?: response.code.toString() } - fun lnAddressInvoice( - lnaddress: String, + suspend fun lnAddressInvoice( + lnAddress: String, milliSats: Long, message: String, - nostrRequest: String? = null, + nostrRequest: LnZapRequestEvent? = null, okHttpClient: (String) -> OkHttpClient, - onSuccess: (String) -> Unit, - onError: (String, String) -> Unit, onProgress: (percent: Float) -> Unit, context: Context, - ) { + ): String { val mapper = jacksonObjectMapper() - fetchLightningAddressJson( - lnaddress, - okHttpClient, - onSuccess = { lnAddressJson -> - onProgress(0.4f) + val lnAddressJson = + fetchLightningAddressJson( + lnAddress, + okHttpClient, + context, + ) - val lnurlp = - try { - mapper.readTree(lnAddressJson) - } catch (t: Throwable) { - if (t is CancellationException) throw t - onError( - stringRes(context, R.string.error_unable_to_fetch_invoice), - stringRes( - context, - R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user, - lnaddress, - ), - ) - null - } + onProgress(0.4f) - val callback = lnurlp?.get("callback")?.asText()?.ifBlank { null } - - if (callback == null) { - onError( - stringRes(context, R.string.error_unable_to_fetch_invoice), - stringRes( - context, - R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user, - lnaddress, - ), - ) - } - - val allowsNostr = lnurlp?.get("allowsNostr")?.asBoolean() ?: false - - callback?.let { cb -> - fetchLightningInvoice( - cb, - milliSats, - message, - if (allowsNostr) nostrRequest else null, - okHttpClient, - onSuccess = { - onProgress(0.6f) - - val lnInvoice = - try { - mapper.readTree(it) - } catch (t: Throwable) { - if (t is CancellationException) throw t - onError( - stringRes(context, R.string.error_unable_to_fetch_invoice), - stringRes( - context, - R.string - .error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user, - lnaddress, - ), - ) - null - } - - lnInvoice - ?.get("pr") - ?.asText() - ?.ifBlank { null } - ?.let { pr -> - // Forces LN Invoice amount to be the requested amount. - val expectedAmountInSats = - BigDecimal(milliSats).divide(BigDecimal(1000), RoundingMode.HALF_UP).toLong() - val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr) - if (invoiceAmount.toLong() == expectedAmountInSats) { - onProgress(0.7f) - onSuccess(pr) - } else { - onProgress(0.0f) - onError( - stringRes(context, R.string.error_unable_to_fetch_invoice), - stringRes( - context, - R.string.incorrect_invoice_amount_sats_from_it_should_have_been, - invoiceAmount.toLong().toString(), - lnaddress, - expectedAmountInSats.toString(), - ), - ) - } - } - ?: lnInvoice - ?.get("reason") - ?.asText() - ?.ifBlank { null } - ?.let { reason -> - onProgress(0.0f) - onError( - stringRes(context, R.string.error_unable_to_fetch_invoice), - stringRes( - context, - R.string - .unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user, - lnaddress, - reason, - ), - ) - } - ?: run { - onProgress(0.0f) - onError( - stringRes(context, R.string.error_unable_to_fetch_invoice), - stringRes( - context, - R.string - .unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user, - lnaddress, - ), - ) - } - }, - onError = onError, + val lnurlp = + try { + mapper.readTree(lnAddressJson) + } catch (t: Throwable) { + if (t is CancellationException) throw t + throw LightningAddressError( + stringRes(context, R.string.error_unable_to_fetch_invoice), + stringRes( context, - ) - } - }, - onError = onError, - context, - ) + R.string.error_parsing_json_from_lightning_address_check_the_user_s_lightning_setup_with_user, + lnAddress, + ), + ) + } + + val callbackUrl = lnurlp?.get("callback")?.asText()?.ifBlank { null } + + if (callbackUrl == null) { + throw LightningAddressError( + stringRes(context, R.string.error_unable_to_fetch_invoice), + stringRes( + context, + R.string.callback_url_not_found_in_the_user_s_lightning_address_server_configuration_with_user, + lnAddress, + ), + ) + } + + val allowsNostr = lnurlp.get("allowsNostr")?.asBoolean() ?: false + + val invoice = + fetchLightningInvoice( + lnCallback = callbackUrl, + milliSats = milliSats, + message = message, + nostrRequest = if (allowsNostr) nostrRequest else null, + okHttpClient = okHttpClient, + context = context, + ) + + onProgress(0.6f) + + val lnInvoice = + try { + mapper.readTree(invoice) + } catch (t: Throwable) { + if (t is CancellationException) throw t + throw LightningAddressError( + stringRes(context, R.string.error_unable_to_fetch_invoice), + stringRes( + context, + R.string + .error_parsing_json_from_lightning_address_s_invoice_fetch_check_the_user_s_lightning_setup_with_user, + lnAddress, + ), + ) + } + + val pr = lnInvoice?.get("pr")?.asText()?.ifBlank { null } + + if (pr == null) { + onProgress(0.0f) + val reason = lnInvoice?.get("reason")?.asText()?.ifBlank { null } + + if (reason != null) { + throw LightningAddressError( + stringRes(context, R.string.error_unable_to_fetch_invoice), + stringRes( + context, + R.string + .unable_to_create_a_lightning_invoice_before_sending_the_zap_the_receiver_s_lightning_wallet_sent_the_following_error_with_user, + lnAddress, + reason, + ), + ) + } else { + throw LightningAddressError( + stringRes(context, R.string.error_unable_to_fetch_invoice), + stringRes( + context, + R.string + .unable_to_create_a_lightning_invoice_before_sending_the_zap_element_pr_not_found_in_the_resulting_json_with_user, + lnAddress, + ), + ) + } + } + + // Forces LN Invoice amount to be the requested amount. + val expectedAmountInSats = + BigDecimal(milliSats).divide(BigDecimal(1000), RoundingMode.HALF_UP).toLong() + + val invoiceAmount = LnInvoiceUtil.getAmountInSats(pr) + + if (invoiceAmount.toLong() != expectedAmountInSats) { + onProgress(0.0f) + throw LightningAddressError( + stringRes(context, R.string.error_unable_to_fetch_invoice), + stringRes( + context, + R.string.incorrect_invoice_amount_sats_from_it_should_have_been, + invoiceAmount.toLong().toString(), + lnAddress, + expectedAmountInSats.toString(), + ), + ) + } + + onProgress(0.7f) + + return pr } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 9a43fb3981..14001c6a2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -25,11 +25,12 @@ import com.vitorpamplona.amethyst.AccountInfo import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal -import com.vitorpamplona.quartz.utils.launchAndWaitAll +import com.vitorpamplona.quartz.utils.mapNotNullAsync import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -37,12 +38,14 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.coroutines.executeAsync import kotlin.coroutines.resume class RegisterAccounts( private val accounts: List, private val client: (String) -> OkHttpClient, ) { + @Suppress("SENSELESS_COMPARISON") val tag = if (BuildConfig.FLAVOR == "play") { "RegisterAccounts FirebaseMsgService" @@ -52,67 +55,64 @@ class RegisterAccounts( private suspend fun signAllAuths( notificationToken: String, - remainingTos: List>>, - output: MutableList, - onReady: (List) -> Unit, - ) { + remainingTos: List, + ): List { if (remainingTos.isEmpty()) { - onReady(output) - return + return emptyList() } - launchAndWaitAll(remainingTos) { accountRelayPair -> - val result = - tryAndWait { continuation -> - val signer = accountRelayPair.first.createSigner() - // TODO: Modify the external launcher to launch as different users. - // Right now it only registers if Amber has already approved this signature - if (signer is NostrSignerExternal) { - signer.launcher.registerLauncher( - launcher = { }, - contentResolver = Amethyst.instance::contentResolverFn, - ) - } - - RelayAuthEvent.create(accountRelayPair.second, notificationToken, signer) { result -> - continuation.resume(result) - } + return mapNotNullAsync(remainingTos) { info -> + tryAndWait { continuation -> + val signer = info.accountSettings.createSigner() + // TODO: Modify the external launcher to launch as different users. + // Right now it only registers if Amber has already approved this signature + if (signer is NostrSignerExternal) { + signer.launcher.registerLauncher( + launcher = { }, + contentResolver = Amethyst.instance::contentResolverFn, + ) } - if (result != null) { - output.add(result) + RelayAuthEvent.create(info.relays, notificationToken, signer) { result -> + continuation.resume(result) + } } } - - onReady(output) } + class Registration( + val accountSettings: AccountSettings, + val relays: List, + ) + // creates proof that it controls all accounts private suspend fun signEventsToProveControlOfAccounts( accounts: List, notificationToken: String, - onReady: (List) -> Unit, - ) { + ): List { val readyToSend = accounts - .mapNotNull { - if (it.hasPrivKey || it.loggedInWithExternalSigner) { - Log.d(tag, "Register Account ${it.npub}") + .mapNotNull { account -> + if (account.hasPrivKey || account.loggedInWithExternalSigner) { + Log.d(tag, "Register Account ${account.npub}") - val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub) + val acc = LocalPreferences.loadCurrentAccountFromEncryptedStorage(account.npub) if (acc != null && acc.isWriteable()) { val nip65Read = acc.backupNIP65RelayList?.readRelaysNorm() ?: emptyList() - - Log.d(tag, "Register Account ${it.npub} NIP65 Reads ${nip65Read.joinToString(", ") { it.url } }") - val nip17Read = acc.backupDMRelayList?.relays() ?: emptyList() - Log.d(tag, "Register Account ${it.npub} NIP17 Reads ${nip17Read.joinToString(", ") { it.url } }") + if (isDebug) { + val readRelays = nip65Read.joinToString(", ") { it.url } + Log.d(tag, "Register Account ${account.npub} NIP65 Reads $readRelays") + + val dmRelays = nip17Read.joinToString(", ") { it.url } + Log.d(tag, "Register Account ${account.npub} NIP17 Reads $dmRelays") + } val relays = (nip65Read + nip17Read) if (relays.isNotEmpty()) { - Pair(acc, relays) + Registration(acc, relays) } else { null } @@ -124,16 +124,10 @@ class RegisterAccounts( } } - val listOfAuthEvents = mutableListOf() - signAllAuths( - notificationToken, - readyToSend, - listOfAuthEvents, - onReady, - ) + return signAllAuths(notificationToken, readyToSend) } - fun postRegistrationEvent(events: List) { + suspend fun postRegistrationEvent(events: List) { val jsonObject = """{ "events": [ ${events.joinToString(", ") { it.toJson() }} ] @@ -147,19 +141,21 @@ class RegisterAccounts( val request = Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(url) .post(body) .build() - val isSucess = client(url).newCall(request).execute().use { it.isSuccessful } - Log.i(tag, "Server registration $isSucess") + val client = client(url) + + client.newCall(request).executeAsync().use { response -> + Log.i(tag, "Server registration ${response.isSuccessful}") + } } suspend fun go(notificationToken: String) { if (notificationToken.isNotEmpty()) { withContext(Dispatchers.IO) { - signEventsToProveControlOfAccounts(accounts, notificationToken) { postRegistrationEvent(it) } + postRegistrationEvent(signEventsToProveControlOfAccounts(accounts, notificationToken)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBitcoinExplorer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBitcoinExplorer.kt index fec1f9e15c..7ec04b0e9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBitcoinExplorer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpBitcoinExplorer.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.service.ots import android.util.Log import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.quartz.nip03Timestamp.ots.BitcoinExplorer import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException @@ -51,7 +50,6 @@ class OkHttpBitcoinExplorer( val request = Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .header("Accept", "application/json") .url(url) .get() @@ -95,7 +93,6 @@ class OkHttpBitcoinExplorer( val request = Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(url) .get() .build() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt index caf0f73b87..b8af7b7efc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendar.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.ots -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendar import com.vitorpamplona.quartz.nip03Timestamp.ots.StreamDeserializationContext import com.vitorpamplona.quartz.nip03Timestamp.ots.Timestamp @@ -60,7 +59,6 @@ class OkHttpCalendar( val request = okhttp3.Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .header("Accept", "application/vnd.opentimestamps.v1") .header("Content-Type", "application/x-www-form-urlencoded") .url(url) @@ -112,7 +110,6 @@ class OkHttpCalendar( val request = okhttp3.Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .header("Accept", "application/vnd.opentimestamps.v1") .header("Content-Type", "application/x-www-form-urlencoded") .url(url) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt index 0feae9e871..c412fc0c00 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpCalendarAsyncSubmit.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.ots -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.quartz.nip03Timestamp.ots.ICalendarAsyncSubmit import com.vitorpamplona.quartz.nip03Timestamp.ots.StreamDeserializationContext import com.vitorpamplona.quartz.nip03Timestamp.ots.Timestamp @@ -45,7 +44,6 @@ class OkHttpCalendarAsyncSubmit( val request = okhttp3.Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .header("Accept", "application/vnd.opentimestamps.v1") .header("Content-Type", "application/x-www-form-urlencoded") .url(url) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt index 2749a52e1d..e2f95fadbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/controls/RenderControlButtons.kt @@ -20,19 +20,16 @@ */ package com.vitorpamplona.amethyst.service.playback.composable.controls -import android.content.Context import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData import com.vitorpamplona.amethyst.service.playback.diskCache.isLiveStreaming import com.vitorpamplona.amethyst.service.playback.pip.PipVideoActivity -import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk import com.vitorpamplona.amethyst.ui.components.ShareImageAction import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -70,7 +67,7 @@ fun RenderControlButtons( if (!isLiveStreaming(mediaData.videoUri)) { AnimatedSaveButton(controllerVisible, buttonPositionModifier.padding(end = Size110dp)) { context -> - saveMediaToGalleryInner(mediaData.videoUri, mediaData.mimeType, context, accountViewModel) + accountViewModel.saveMediaToGallery(mediaData.videoUri, mediaData.mimeType, context) } AnimatedShareButton(controllerVisible, buttonPositionModifier.padding(end = Size165dp)) { popupExpanded, toggle -> @@ -82,23 +79,3 @@ fun RenderControlButtons( } } } - -private fun saveMediaToGalleryInner( - videoUri: String?, - mimeType: String?, - localContext: Context, - accountViewModel: AccountViewModel, -) { - MediaSaverToDisk.saveDownloadingIfNeeded( - videoUri = videoUri, - okHttpClient = accountViewModel::okHttpClientForVideo, - mimeType = mimeType, - localContext = localContext, - onSuccess = { - accountViewModel.toastManager.toast(R.string.video_saved_to_the_gallery, R.string.video_saved_to_the_gallery) - }, - onError = { - accountViewModel.toastManager.toast(R.string.failed_to_save_the_video, null, it) - }, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt index 4508340ff9..3d25f38866 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/previews/UrlPreview.kt @@ -20,13 +20,13 @@ */ package com.vitorpamplona.amethyst.service.previews -import com.vitorpamplona.amethyst.service.checkNotInMainThread import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.coroutines.executeAsync class UrlPreview { suspend fun fetch( @@ -52,24 +52,29 @@ class UrlPreview { .url(url) .get() .build() - okHttpClient(url).newCall(request).execute().use { - checkNotInMainThread() - if (it.isSuccessful) { - val mimeType = - it.headers["Content-Type"]?.toMediaType() - ?: throw IllegalArgumentException("Website returned unknown mimetype: ${it.headers["Content-Type"]}") - if (mimeType.type == "text" && mimeType.subtype == "html") { - val data = OpenGraphParser().extractUrlInfo(HtmlParser().parseHtml(it.body.source(), mimeType)) - UrlInfoItem(url, data.title, data.description, data.image, mimeType.toString()) - } else if (mimeType.type == "image") { - UrlInfoItem(url, image = url, mimeType = mimeType.toString()) - } else if (mimeType.type == "video") { - UrlInfoItem(url, image = url, mimeType = mimeType.toString()) + + val client = okHttpClient(url) + + client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (response.isSuccessful) { + val mimeType = + response.headers["Content-Type"]?.toMediaType() + ?: throw IllegalArgumentException("Website returned unknown mimetype: ${response.headers["Content-Type"]}") + if (mimeType.type == "text" && mimeType.subtype == "html") { + val metaTags = HtmlParser().parseHtml(response.body.source(), mimeType) + val data = OpenGraphParser().extractUrlInfo(metaTags) + UrlInfoItem(url, data.title, data.description, data.image, mimeType.toString()) + } else if (mimeType.type == "image") { + UrlInfoItem(url, image = url, mimeType = mimeType.toString()) + } else if (mimeType.type == "video") { + UrlInfoItem(url, image = url, mimeType = mimeType.toString()) + } else { + throw IllegalArgumentException("Website returned unknown encoding for previews: $mimeType") + } } else { - throw IllegalArgumentException("Website returned unknown encoding for previews: $mimeType") + throw IllegalArgumentException("Website returned: " + response.code) } - } else { - throw IllegalArgumentException("Website returned: " + it.code) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt index 6e9ab4cec9..7979093cbd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/BlossomUploader.kt @@ -27,7 +27,6 @@ import android.provider.OpenableColumns import android.webkit.MimeTypeMap import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.checkNotInMainThread @@ -38,10 +37,13 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.sha256.sha256 +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody +import okhttp3.coroutines.executeAsync import okio.BufferedSink import okio.source import java.io.File @@ -157,29 +159,28 @@ class BlossomUploader { requestBuilder .addHeader("Content-Length", length.toString()) - .addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(apiUrl) .put(requestBody) val request = requestBuilder.build() - client.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.body.use { body -> - val str = body.string() - val result = parseResults(str) - return result - } - } else { - val errorMessage = response.headers.get("X-Reason") - - val explanation = HttpStatusMessages.resourceIdFor(response.code) - if (errorMessage != null) { - throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), errorMessage)) - } else if (explanation != null) { - throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), stringRes(context, explanation))) + return client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (response.isSuccessful) { + response.body.use { body -> + parseResults(body.string()) + } } else { - throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), response.code.toString())) + val errorMessage = response.headers.get("X-Reason") + + val explanation = HttpStatusMessages.resourceIdFor(response.code) + if (errorMessage != null) { + throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), errorMessage)) + } else if (explanation != null) { + throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), stringRes(context, explanation))) + } else { + throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, serverBaseUrl.displayUrl(), response.code.toString())) + } } } } @@ -208,20 +209,21 @@ class BlossomUploader { val request = requestBuilder - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(apiUrl.removeSuffix("/") + "/$hash.$extension") .delete() .build() - okHttpClient(apiUrl).newCall(request).execute().use { response -> - if (response.isSuccessful) { - return true - } else { - val explanation = HttpStatusMessages.resourceIdFor(response.code) - if (explanation != null) { - throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, stringRes(context, explanation))) + return okHttpClient(apiUrl).newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (response.isSuccessful) { + true } else { - throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, response.code)) + val explanation = HttpStatusMessages.resourceIdFor(response.code) + if (explanation != null) { + throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, stringRes(context, explanation))) + } else { + throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, response.code)) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt index a6dca1bda7..b6acdfde82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/Nip96Uploader.kt @@ -27,7 +27,6 @@ import android.provider.OpenableColumns import android.webkit.MimeTypeMap import androidx.core.net.toFile import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.HttpStatusMessages import com.vitorpamplona.amethyst.service.checkNotInMainThread @@ -41,12 +40,15 @@ import com.vitorpamplona.quartz.nip96FileStorage.actions.UploadResult import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfo import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.utils.RandomInstance +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType import okhttp3.MultipartBody import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody +import okhttp3.coroutines.executeAsync import okio.BufferedSink import okio.source import java.io.InputStream @@ -168,53 +170,54 @@ class Nip96Uploader { httpAuth(server.apiUrl, "POST", null)?.let { requestBuilder.addHeader("Authorization", it.toAuthToken()) } requestBuilder - .addHeader("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(server.apiUrl) .post(requestBody) val request = requestBuilder.build() - client.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.body.use { body -> - val result = UploadResult.parse(body.string()) - if (!result.processingUrl.isNullOrBlank()) { - return waitProcessing(result, server, okHttpClient, onProgress) - } else if (result.status == "success") { - val event = result.nip94Event - if (event != null) { - return convertToMediaResult(event) + return client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (response.isSuccessful) { + response.body.use { body -> + val result = UploadResult.parse(body.string()) + if (!result.processingUrl.isNullOrBlank()) { + waitProcessing(result, server, okHttpClient, onProgress) + } else if (result.status == "success") { + val event = result.nip94Event + if (event != null) { + convertToMediaResult(event) + } else { + throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), result.message)) + } } else { throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), result.message)) } - } else { - throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), result.message)) } - } - } else { - val msg = response.body.string() + } else { + val msg = response.body.string() - val errorMessage = - try { - val tree = jacksonObjectMapper().readTree(msg) - val status = tree.get("status")?.asText() - val message = tree.get("message")?.asText() - if (status == "error" && message != null) { - message - } else { + val errorMessage = + try { + val tree = jacksonObjectMapper().readTree(msg) + val status = tree.get("status")?.asText() + val message = tree.get("message")?.asText() + if (status == "error" && message != null) { + message + } else { + null + } + } catch (e: Exception) { null } - } catch (e: Exception) { - null - } - val explanation = HttpStatusMessages.resourceIdFor(response.code) - if (errorMessage != null) { - throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), errorMessage)) - } else if (explanation != null) { - throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), stringRes(context, explanation))) - } else { - throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), response.code.toString())) + val explanation = HttpStatusMessages.resourceIdFor(response.code) + if (errorMessage != null) { + throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), errorMessage)) + } else if (explanation != null) { + throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), stringRes(context, explanation))) + } else { + throw RuntimeException(stringRes(context, R.string.failed_to_upload_to_server_with_message, server.apiUrl.displayUrl(), response.code.toString())) + } } } } @@ -275,23 +278,22 @@ class Nip96Uploader { val request = requestBuilder - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(server.apiUrl.removeSuffix("/") + "/$hash.$extension") .delete() .build() - client.newCall(request).execute().use { response -> - if (response.isSuccessful) { - response.body.use { body -> - val result = DeleteResult.parse(body.string()) - return result.status == "success" - } - } else { - val explanation = HttpStatusMessages.resourceIdFor(response.code) - if (explanation != null) { - throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, stringRes(context, explanation))) + return client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (response.isSuccessful) { + val result = DeleteResult.parse(response.body.string()) + result.status == "success" } else { - throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, response.code)) + val explanation = HttpStatusMessages.resourceIdFor(response.code) + if (explanation != null) { + throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, stringRes(context, explanation))) + } else { + throw RuntimeException(stringRes(context, R.string.failed_to_delete_with_message, response.code)) + } } } } @@ -313,14 +315,15 @@ class Nip96Uploader { val request: Request = Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .url(procUrl) .build() val client = okHttpClient(procUrl) - client.newCall(request).execute().use { - if (it.isSuccessful) { - it.body.use { currentResult = UploadResult.parse(it.string()) } + client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (response.isSuccessful) { + currentResult = UploadResult.parse(response.body.string()) + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt index f3abddafc8..e473a9424d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt @@ -21,12 +21,14 @@ package com.vitorpamplona.amethyst.service.uploads.nip96 import android.util.Log -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfo import com.vitorpamplona.quartz.nip96FileStorage.info.ServerInfoParser import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.coroutines.executeAsync class ServerInfoRetriever { val parser = ServerInfoParser() @@ -42,24 +44,25 @@ class ServerInfoRetriever { .url(parser.assembleUrl(baseUrl)) .build() - okHttpClient(baseUrl).newCall(request).execute().use { response -> - checkNotInMainThread() - response.use { - val body = it.body.string() - try { - if (it.isSuccessful) { - return parser.parse(baseUrl, body) + val client = okHttpClient(baseUrl) + + return try { + client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + if (response.isSuccessful) { + val body = response.body.string() + parser.parse(baseUrl, body) } else { throw RuntimeException( "Resulting Message from $baseUrl is an error: ${response.code} ${response.message}", ) } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("RelayInfoFail", "Resulting Message from $baseUrl in not parseable: $body", e) - throw e } } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("RelayInfoFail", "Resulting Message from $baseUrl", e) + throw e } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 6a7781ecea..67a6a9f682 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -54,11 +54,9 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier @@ -92,7 +90,6 @@ import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest -import com.vitorpamplona.amethyst.ui.note.creators.messagefield.MessageField import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -315,11 +312,11 @@ fun EditPostView( ) { InvoiceRequest( lud16, - user.pubkeyHex, + user, accountViewModel, stringRes(id = R.string.lightning_invoice), stringRes(id = R.string.lightning_create_and_add_invoice), - onSuccess = { + onNewInvoice = { postViewModel.message = TextFieldValue(postViewModel.message.text + "\n\n" + it) postViewModel.wantsInvoice = false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt index d3e3e3b8ba..42773706c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt @@ -27,20 +27,21 @@ import android.media.MediaScannerConnection import android.os.Build import android.os.Environment import android.provider.MediaStore +import android.util.Log import android.webkit.MimeTypeMap import androidx.annotation.RequiresApi import androidx.core.net.toFile import androidx.core.net.toUri -import com.vitorpamplona.amethyst.BuildConfig +import coil3.util.MimeTypeMap.getMimeTypeFromExtension import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk.PICTURES_SUBDIRECTORY +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError import kotlinx.coroutines.CancellationException -import okhttp3.Call -import okhttp3.Callback +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request -import okhttp3.Response +import okhttp3.coroutines.executeAsync import okio.BufferedSource -import okio.IOException import okio.buffer import okio.sink import okio.source @@ -48,7 +49,7 @@ import java.io.File import java.util.UUID object MediaSaverToDisk { - fun saveDownloadingIfNeeded( + suspend fun saveDownloadingIfNeeded( videoUri: String?, okHttpClient: (String) -> OkHttpClient, mimeType: String?, @@ -83,7 +84,7 @@ object MediaSaverToDisk { * * @see PICTURES_SUBDIRECTORY */ - fun downloadAndSave( + suspend fun downloadAndSave( url: String, mimeType: String?, okHttpClient: (String) -> OkHttpClient, @@ -92,67 +93,50 @@ object MediaSaverToDisk { onError: (Throwable) -> Any?, ) { val client = okHttpClient(url) - val request = Request .Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") .get() .url(url) .build() - client - .newCall(request) - .enqueue( - object : Callback { - override fun onFailure( - call: Call, - e: IOException, - ) { - e.printStackTrace() - onError(e) - } + try { + client.newCall(request).executeAsync().use { response -> + withContext(Dispatchers.IO) { + check(response.isSuccessful) - override fun onResponse( - call: Call, - response: Response, - ) { - try { - check(response.isSuccessful) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val contentType = response.header("Content-Type") ?: getMimeTypeFromExtension(trimInlineMetaData(url)) + check(contentType.isNotBlank()) { "Can't find out the content type" } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val contentType = response.header("Content-Type") ?: getMimeTypeFromExtension(trimInlineMetaData(url)) - check(contentType.isNotBlank()) { "Can't find out the content type" } - - val realType = - if (contentType == "application/octet-stream") { - mimeType ?: getMimeTypeFromExtension(url) - } else { - contentType - } - - saveContentQ( - displayName = File(trimInlineMetaData(url)).nameWithoutExtension, - contentType = realType, - contentSource = response.body.source(), - contentResolver = context.contentResolver, - ) + val realType = + if (contentType == "application/octet-stream") { + mimeType ?: getMimeTypeFromExtension(url) } else { - saveContentDefault( - fileName = File(trimInlineMetaData(url)).name, - contentSource = response.body.source(), - context = context, - ) + contentType } - onSuccess() - } catch (e: Exception) { - if (e is CancellationException) throw e - e.printStackTrace() - onError(e) - } + + saveContentQ( + displayName = File(trimInlineMetaData(url)).nameWithoutExtension, + contentType = realType, + contentSource = response.body.source(), + contentResolver = context.contentResolver, + ) + } else { + saveContentDefault( + fileName = File(trimInlineMetaData(url)).name, + contentSource = response.body.source(), + context = context, + ) } - }, - ) + onSuccess() + } + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("MediaSaverToDisk", "Error parsing response", e) + onError(e) + } } private fun getMimeTypeFromExtension(fileName: String): String = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt index 183cafa844..55ccb38714 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/CashuRedeem.kt @@ -56,8 +56,8 @@ import androidx.core.net.toUri import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.Cashu import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons -import com.vitorpamplona.amethyst.service.CachedCashuProcessor -import com.vitorpamplona.amethyst.service.CashuToken +import com.vitorpamplona.amethyst.service.cashu.CachedCashuParser +import com.vitorpamplona.amethyst.service.cashu.CashuToken import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.note.CopyIcon import com.vitorpamplona.amethyst.ui.note.OpenInNewIcon @@ -82,10 +82,10 @@ fun CashuPreview( ) { @Suppress("ProduceStateDoesNotAssignValue") val cashuData by produceState( - initialValue = CachedCashuProcessor.cached(cashutoken), + initialValue = CachedCashuParser.cached(cashutoken), key1 = cashutoken, ) { - val newToken = withContext(Dispatchers.Default) { CachedCashuProcessor.parse(cashutoken) } + val newToken = withContext(Dispatchers.Default) { CachedCashuParser.parse(cashutoken) } if (value != newToken) { value = newToken } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt index 087ec6b503..bfc5b8a36e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentDialog.kt @@ -265,7 +265,7 @@ private fun DialogContent( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q || writeStoragePermissionState.status.isGranted ) { - scope.launch { + scope.launch(Dispatchers.IO) { saveMediaToGallery(myContent, localContext, accountViewModel) } scope.launch { @@ -295,7 +295,7 @@ private fun DialogContent( } } -private fun saveMediaToGallery( +private suspend fun saveMediaToGallery( content: BaseMediaContent, localContext: Context, accountViewModel: AccountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index b6459f1c3e..0ebb39184b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -84,7 +84,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CancellationException -class ZapOptionstViewModel : ViewModel() { +class ZapOptionViewModel : ViewModel() { private var account: Account? = null var customAmount by mutableStateOf(TextFieldValue("21")) @@ -111,7 +111,7 @@ fun ZapCustomDialog( baseNote: Note, ) { val context = LocalContext.current - val postViewModel: ZapOptionstViewModel = viewModel() + val postViewModel: ZapOptionViewModel = viewModel() LaunchedEffect(accountViewModel) { postViewModel.load(accountViewModel.account) } @@ -214,11 +214,7 @@ fun ZapCustomDialog( TextSpinner( label = stringRes(id = R.string.zap_type), - placeholder = - zapTypes - .filter { it.first == accountViewModel.defaultZapType() } - .first() - .second, + placeholder = zapTypes.first { it.first == accountViewModel.defaultZapType() }.second, options = zapOptions, onSelect = { selectedZapType = zapTypes[it].first }, modifier = Modifier.weight(1f).padding(end = 5.dp), @@ -232,15 +228,16 @@ fun ZapCustomDialog( OutlinedTextField( // stringRes(R.string.new_amount_in_sats label = { - if ( - selectedZapType == LnZapEvent.ZapType.PUBLIC || - selectedZapType == LnZapEvent.ZapType.ANONYMOUS - ) { - Text(text = stringRes(id = R.string.custom_zaps_add_a_message)) - } else if (selectedZapType == LnZapEvent.ZapType.PRIVATE) { - Text(text = stringRes(id = R.string.custom_zaps_add_a_message_private)) - } else if (selectedZapType == LnZapEvent.ZapType.NONZAP) { - Text(text = stringRes(id = R.string.custom_zaps_add_a_message_nonzap)) + when (selectedZapType) { + LnZapEvent.ZapType.PUBLIC, LnZapEvent.ZapType.ANONYMOUS -> { + Text(text = stringRes(id = R.string.custom_zaps_add_a_message)) + } + LnZapEvent.ZapType.PRIVATE -> { + Text(text = stringRes(id = R.string.custom_zaps_add_a_message_private)) + } + LnZapEvent.ZapType.NONZAP -> { + Text(text = stringRes(id = R.string.custom_zaps_add_a_message_nonzap)) + } } }, value = postViewModel.customMessage, @@ -295,7 +292,7 @@ fun PayViaIntentDialog( if (payingInvoices.size == 1) { val payable = payingInvoices.first() payViaIntent(payable.invoice, context, onClose) { - onError(UserBasedErrorMessage(it, payable.user)) + onError(UserBasedErrorMessage(it, payable.info.user)) } } else { Dialog( @@ -326,8 +323,8 @@ fun PayViaIntentDialog( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = Size10dp), ) { - if (payable.user != null) { - BaseUserPicture(payable.user, Size55dp, accountViewModel = accountViewModel) + if (payable.info.user != null) { + BaseUserPicture(payable.info.user, Size55dp, accountViewModel = accountViewModel) } else { DisplayBlankAuthor(size = Size55dp, accountViewModel = accountViewModel) } @@ -335,8 +332,8 @@ fun PayViaIntentDialog( Spacer(modifier = DoubleHorzSpacer) Column(modifier = Modifier.weight(1f)) { - if (payable.user != null) { - UsernameDisplay(payable.user, accountViewModel = accountViewModel) + if (payable.info.user != null) { + UsernameDisplay(payable.info.user, accountViewModel = accountViewModel) } else { Text( text = stringRes(id = R.string.wallet_number, index + 1), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt index 13080fc997..59baf599eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/InvoiceRequest.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.hashtags.Lightning +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness @@ -63,7 +64,7 @@ import com.vitorpamplona.amethyst.ui.theme.subtleBorder @Composable fun InvoiceRequestCard( lud16: String, - toUserPubKeyHex: String, + user: User, accountViewModel: AccountViewModel, titleText: String? = null, buttonText: String? = null, @@ -81,7 +82,7 @@ fun InvoiceRequestCard( ) { InvoiceRequest( lud16, - toUserPubKeyHex, + user, accountViewModel, titleText, buttonText, @@ -94,11 +95,11 @@ fun InvoiceRequestCard( @Composable fun InvoiceRequest( lud16: String, - toUserPubKeyHex: String, + user: User, accountViewModel: AccountViewModel, titleText: String? = null, buttonText: String? = null, - onSuccess: (String) -> Unit, + onNewInvoice: (String) -> Unit, onError: (String, String) -> Unit, ) { val context = LocalContext.current @@ -176,11 +177,11 @@ fun InvoiceRequest( modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp), onClick = { accountViewModel.sendSats( - lnaddress = lud16, + lnAddress = lud16, + user = user, milliSats = amount * 1000, message = message, - toUserPubKeyHex = toUserPubKeyHex, - onSuccess = onSuccess, + onNewInvoice = onNewInvoice, onError = onError, onProgress = {}, context = context, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt index 5ee80d2f14..a2e04533e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/invoice/NewPostInvoiceRequest.kt @@ -30,15 +30,21 @@ fun NewPostInvoiceRequest( onSuccess: (String) -> Unit, accountViewModel: AccountViewModel, ) { - accountViewModel.account.userProfile().info?.lnAddress()?.let { lud16 -> + val lnAddress = + accountViewModel.account + .userProfile() + .info + ?.lnAddress() + + if (lnAddress != null) { InvoiceRequest( - lud16, - accountViewModel.account.userProfile().pubkeyHex, - accountViewModel, - stringRes(id = R.string.lightning_invoice), - stringRes(id = R.string.lightning_create_and_add_invoice), - onSuccess = onSuccess, - onError = { title, message -> accountViewModel.toastManager.toast(title, message) }, + lud16 = lnAddress, + user = accountViewModel.account.userProfile(), + accountViewModel = accountViewModel, + titleText = stringRes(id = R.string.lightning_invoice), + buttonText = stringRes(id = R.string.lightning_create_and_add_invoice), + onNewInvoice = onSuccess, + onError = accountViewModel.toastManager::toast, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index f91fb778cd..2990dcd044 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -321,11 +321,11 @@ private fun GenericCommentPostBody( postViewModel.lnAddress()?.let { lud16 -> InvoiceRequest( lud16, - accountViewModel.account.userProfile().pubkeyHex, + accountViewModel.account.userProfile(), accountViewModel, stringRes(id = R.string.lightning_invoice), stringRes(id = R.string.lightning_create_and_add_invoice), - onSuccess = { + onNewInvoice = { postViewModel.insertAtCursor(it) postViewModel.wantsInvoice = false }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 101800e4d0..1faadedf2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -233,7 +233,7 @@ class AccountStateViewModel : ViewModel() { } else if (EMAIL_PATTERN.matcher(key).matches()) { Nip05NostrAddressVerifier().verifyNip05( key, - okttpClient = { Amethyst.instance.okHttpClients.getHttpClient(false) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(false) }, onSuccess = { publicKey -> loginSync(Hex.decode(publicKey).toNpub(), torSettings, transientAccount, loginWithExternalSigner, packageName, onError) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 9919b0f6f5..d42ff041ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -56,16 +56,17 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.WarningType import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator -import com.vitorpamplona.amethyst.service.CashuProcessor -import com.vitorpamplona.amethyst.service.CashuToken import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.Nip11Retriever import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.ZapPaymentHandler +import com.vitorpamplona.amethyst.service.cashu.CashuToken +import com.vitorpamplona.amethyst.service.cashu.melt.MeltProcessor import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.ui.actions.Dao +import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager import com.vitorpamplona.amethyst.ui.feeds.FeedState @@ -125,7 +126,8 @@ import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.collectSuccessfulOperations +import com.vitorpamplona.quartz.utils.mapNotNullAsync +import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf @@ -148,6 +150,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient +import kotlin.coroutines.resume @Stable class AccountViewModel( @@ -480,22 +483,21 @@ class AccountViewModel( ) }.toMutableMap() - collectSuccessfulOperations( - items = zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true }, - runRequestFor = { next, onReady -> - checkNotInMainThread() - - innerDecryptAmountMessage(next.request, next.response) { - onReady(DecryptedInfo(next.request, next.response, it)) + val results = + mapNotNullAsync( + zaps.filter { (it.request.event as? LnZapRequestEvent)?.isPrivateZap() == true }, + ) { next -> + val info = innerDecryptAmountMessage(next.request, next.response) + if (info != null) { + DecryptedInfo(next.request, next.response, info) + } else { + null } - }, - ) { - checkNotInMainThread() + } - it.forEach { decrypted -> initialResults[decrypted.zapRequest] = decrypted.info } + results.forEach { decrypted -> initialResults[decrypted.zapRequest] = decrypted.info } - onNewState(initialResults.values.toImmutableList()) - } + onNewState(initialResults.values.toImmutableList()) } } @@ -586,18 +588,19 @@ class AccountViewModel( ) }.toMutableMap() - collectSuccessfulOperations, DecryptedInfo>( - items = myList, - runRequestFor = { next, onReady -> - innerDecryptAmountMessage(next.first, next.second) { - onReady(DecryptedInfo(next.first, next.second, it)) + val decryptedInfo = + mapNotNullAsync(myList) { next -> + val info = innerDecryptAmountMessage(next.first, next.second) + if (info != null) { + DecryptedInfo(next.first, next.second, info) + } else { + null } - }, - ) { - it.forEach { decrypted -> initialResults[decrypted.zapRequest] = decrypted.info } + } - onNewState(initialResults.values.toImmutableList()) - } + decryptedInfo.forEach { decrypted -> initialResults[decrypted.zapRequest] = decrypted.info } + + onNewState(initialResults.values.toImmutableList()) } } @@ -607,44 +610,39 @@ class AccountViewModel( onNewState: (ZapAmountCommentNotification?) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { - innerDecryptAmountMessage(zapRequest, zapEvent, onNewState) + onNewState(innerDecryptAmountMessage(zapRequest, zapEvent)) } } - private fun innerDecryptAmountMessage( + private suspend fun innerDecryptAmountMessage( zapRequest: Note, zapEvent: Note?, - onReady: (ZapAmountCommentNotification) -> Unit, - ) { - checkNotInMainThread() - + ): ZapAmountCommentNotification? = (zapRequest.event as? LnZapRequestEvent)?.let { + val amount = showAmountInteger((zapEvent?.event as? LnZapEvent)?.amount) if (it.isPrivateZap()) { - decryptZap(zapRequest) { decryptedContent -> - val amount = (zapEvent?.event as? LnZapEvent)?.amount - val newAuthor = LocalCache.getOrCreateUser(decryptedContent.pubKey) - onReady( - ZapAmountCommentNotification( - newAuthor, - decryptedContent.content.ifBlank { null }, - showAmountInteger(amount), - ), + val decryptedContent = account.decryptZapOrNull(it) + if (decryptedContent != null) { + ZapAmountCommentNotification( + LocalCache.checkGetOrCreateUser(decryptedContent.pubKey), + decryptedContent.content.ifBlank { null }, + amount, + ) + } else { + ZapAmountCommentNotification( + zapRequest.author, + null, + amount, ) } } else { - val amount = (zapEvent?.event as? LnZapEvent)?.amount - if (!zapRequest.event?.content.isNullOrBlank() || amount != null) { - onReady( - ZapAmountCommentNotification( - zapRequest.author, - zapRequest.event?.content?.ifBlank { null }, - showAmountInteger(amount), - ), - ) - } + ZapAmountCommentNotification( + zapRequest.author, + zapRequest.event?.content?.ifBlank { null }, + amount, + ) } } - } fun zap( note: Note, @@ -795,13 +793,6 @@ class AccountViewModel( viewModelScope.launch(Dispatchers.IO) { account.decryptContent(note, onReady) } } - fun decryptZap( - note: Note, - onReady: (Event) -> Unit, - ) { - account.decryptZapContentAuthor(note, onReady) - } - fun follow(channel: PublicChatChannel) { viewModelScope.launch(Dispatchers.IO) { account.follow(channel) } } @@ -980,7 +971,7 @@ class AccountViewModel( Nip05NostrAddressVerifier() .verifyNip05( nip05, - okttpClient = { + okHttpClient = { app.okHttpClients.getHttpClient(account.shouldUseTorForNIP05(it)) }, onSuccess = { @@ -1377,15 +1368,26 @@ class AccountViewModel( val lud16 = account.userProfile().info?.lud16 if (lud16 != null) { viewModelScope.launch(Dispatchers.IO) { - CashuProcessor() - .melt( - token, - lud16, - okHttpClient = ::okHttpClientForMoney, - onSuccess = { title, message -> onDone(title, message) }, - onError = { title, message -> onDone(title, message) }, - context, + try { + val meltResult = MeltProcessor().melt(token, lud16, ::okHttpClientForMoney, context) + onDone( + stringRes(context, R.string.cashu_successful_redemption), + stringRes( + context, + R.string.cashu_successful_redemption_explainer, + token.totalAmount.toString(), + meltResult.fees.toString(), + ), ) + } catch (e: LightningAddressResolver.LightningAddressError) { + onDone(e.title, e.msg) + } catch (e: Exception) { + if (e is kotlin.coroutines.cancellation.CancellationException) throw e + onDone( + stringRes(context, R.string.cashu_failed_redemption), + stringRes(context, R.string.cashu_failed_redemption_explainer_error_msg, e.message), + ) + } } } else { onDone( @@ -1606,46 +1608,73 @@ class AccountViewModel( } fun sendSats( - lnaddress: String, + lnAddress: String, + user: User, milliSats: Long, message: String, - toUserPubKeyHex: HexKey, - onSuccess: (String) -> Unit, + onNewInvoice: (String) -> Unit, onError: (String, String) -> Unit, onProgress: (percent: Float) -> Unit, context: Context, ) { viewModelScope.launch(Dispatchers.IO) { - if (defaultZapType() == LnZapEvent.ZapType.NONZAP) { - LightningAddressResolver() - .lnAddressInvoice( - lnaddress, - milliSats, - message, - null, + try { + val zapRequest = prepareZapRequestIfNeeded(user, message, defaultZapType()) + + val invoice = + LightningAddressResolver().lnAddressInvoice( + lnAddress = lnAddress, + milliSats = milliSats, + message = message, + nostrRequest = zapRequest, okHttpClient = ::okHttpClientForMoney, - onSuccess = onSuccess, - onError = onError, onProgress = onProgress, context = context, ) - } else { - account.createZapRequestFor(toUserPubKeyHex, message, defaultZapType()) { zapRequest -> - LocalCache.justConsumeMyOwnEvent(zapRequest) - LightningAddressResolver() - .lnAddressInvoice( - lnaddress, - milliSats, - message, - zapRequest.toJson(), - okHttpClient = ::okHttpClientForMoney, - onSuccess = onSuccess, - onError = onError, - onProgress = onProgress, - context = context, - ) + + onNewInvoice(invoice) + } catch (e: LightningAddressResolver.LightningAddressError) { + onError(e.title, e.msg) + } catch (e: Exception) { + if (e is CancellationException) throw e + onError("Error", e.message ?: "Unknown error") + } + } + } + + private suspend fun prepareZapRequestIfNeeded( + user: User, + message: String, + zapType: LnZapEvent.ZapType, + ): LnZapRequestEvent? = + if (zapType != LnZapEvent.ZapType.NONZAP) { + tryAndWait { continuation -> + account.createZapRequestFor(user, message, zapType) { zapRequest -> + continuation.resume(zapRequest) } } + } else { + null + } + + fun saveMediaToGallery( + videoUri: String?, + mimeType: String?, + localContext: Context, + ) { + viewModelScope.launch { + MediaSaverToDisk.saveDownloadingIfNeeded( + videoUri = videoUri, + okHttpClient = ::okHttpClientForVideo, + mimeType = mimeType, + localContext = localContext, + onSuccess = { + toastManager.toast(R.string.video_saved_to_the_gallery, R.string.video_saved_to_the_gallery) + }, + onError = { + toastManager.toast(R.string.failed_to_save_the_video, null, it) + }, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 94cfbce292..fe301d25c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -43,8 +43,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -294,11 +292,11 @@ private fun NewProductBody( postViewModel.lnAddress()?.let { lud16 -> InvoiceRequest( lud16, - accountViewModel.account.userProfile().pubkeyHex, + accountViewModel.account.userProfile(), accountViewModel, stringRes(id = R.string.lightning_invoice), stringRes(id = R.string.lightning_create_and_add_invoice), - onSuccess = { + onNewInvoice = { postViewModel.insertAtCursor(it) postViewModel.wantsInvoice = false }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index c3203f6dd9..2ebc002103 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -364,11 +364,11 @@ private fun NewPostScreenBody( postViewModel.lnAddress()?.let { lud16 -> InvoiceRequest( lud16, - accountViewModel.account.userProfile().pubkeyHex, + accountViewModel.account.userProfile(), accountViewModel, stringRes(id = R.string.lightning_invoice), stringRes(id = R.string.lightning_create_and_add_invoice), - onSuccess = { + onNewInvoice = { postViewModel.insertAtCursor(it) postViewModel.wantsInvoice = false }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt index e7731decf4..07a8930157 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt @@ -26,13 +26,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.navigation.INav @@ -51,12 +51,11 @@ import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse @Composable fun DisplayLNAddress( lud16: String?, - userHex: String, + user: User, accountViewModel: AccountViewModel, nav: INav, ) { val context = LocalContext.current - val scope = rememberCoroutineScope() var zapExpanded by remember { mutableStateOf(false) } var showErrorMessageDialog by remember { mutableStateOf(null) } @@ -67,7 +66,7 @@ fun DisplayLNAddress( textContent = showErrorMessageDialog ?: "", onClickStartMessage = { nav.nav { - routeToMessage(userHex, showErrorMessageDialog, accountViewModel = accountViewModel) + routeToMessage(user, showErrorMessageDialog, accountViewModel = accountViewModel) } }, onDismiss = { showErrorMessageDialog = null }, @@ -105,7 +104,7 @@ fun DisplayLNAddress( ) { InvoiceRequestCard( lud16, - userHex, + user, accountViewModel, onSuccess = { zapExpanded = false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index 9b5d3f74ba..b1b3b81d81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -186,9 +186,19 @@ fun DrawAdditionalInfo( } } - val lud16 = remember(userState) { user.info?.lud16?.trim() ?: user.info?.lud06?.trim() } - val pubkeyHex = remember { baseUser.pubkeyHex } - DisplayLNAddress(lud16, pubkeyHex, accountViewModel, nav) + val lud16 = + remember(userState) { + userState + ?.user + ?.info + ?.lud16 + ?.trim() ?: userState + ?.user + ?.info + ?.lud06 + ?.trim() + } + DisplayLNAddress(lud16, baseUser, accountViewModel, nav) val identities = user.latestMetadata?.identityClaims() if (!identities.isNullOrEmpty()) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05bf20797a..c227c8ff15 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ media3 = "1.7.1" mockk = "1.14.4" kotlinx-coroutines-test = "1.10.2" navigationCompose = "2.9.1" -okhttp = "5.0.0" +okhttp = "5.1.0" runner = "1.6.2" rfc3986 = "0.1.2" secp256k1KmpJniAndroid = "0.18.0" @@ -118,6 +118,7 @@ markdown-ui-material3 = { group = "com.github.vitorpamplona.compose-richtext", n mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines-test"} okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttpCoroutines = { group = "com.squareup.okhttp3", name = "okhttp-coroutines", version.ref = "okhttp" } rfc3986-normalizer = { group = "org.czeal", name = "rfc3986", version.ref = "rfc3986" } secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt index 04a7d450d2..851869d762 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt @@ -100,7 +100,7 @@ class LnZapRequestEvent( fun decryptPrivateZap( signer: NostrSigner, - onReady: (Event) -> Unit, + onReady: (LnZapPrivateEvent) -> Unit, ) { privateZapEvent?.let { onReady(it) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt index bf46ca8af7..1d41584877 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.quartz.utils +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async +import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.joinAll import kotlinx.coroutines.suspendCancellableCoroutine @@ -48,6 +50,12 @@ suspend fun launchAndWaitAll( withTimeoutOrNull(15000) { jobs.joinAll() } + + async { + jobs.forEach { + it.cancel("Timeout") + } + } } } @@ -69,15 +77,14 @@ suspend inline fun tryAndWait( * runs the request for that item, * and gathers all the results in the output map. */ -suspend fun collectSuccessfulOperations( +suspend fun collectSuccessfulOperationsReturning( items: List, runRequestFor: (T, (K) -> Unit) -> Unit, - output: MutableList = mutableListOf(), - onReady: suspend (List) -> Unit, -) { +): List { + val output: MutableList = mutableListOf() + if (items.isEmpty()) { - onReady(output) - return + return output } launchAndWaitAll(items) { @@ -91,5 +98,58 @@ suspend fun collectSuccessfulOperations( } } - onReady(output) + return output +} + +/** + * Executes multiple suspending functions concurrently using `async` and attempts to wait for all of them + * to complete within a default 15-second timeout. + * + * If the timeout is reached, it returns the results of all tasks that successfully completed by then + * and cancels any tasks that are still running. Tasks that completed with an exception are not + * included in the returned list. + * + * @param tasks A list of suspending functions, each returning a value of type [T]. + * @return A list containing the results of tasks that successfully completed within the timeout. + */ +@OptIn(ExperimentalCoroutinesApi::class) +suspend fun mapNotNullAsync( + items: List, + timeoutMillis: Long = 30000, + runRequestFor: suspend (T) -> K?, +): List { + if (items.isEmpty()) { + return emptyList() + } + + return coroutineScope { + // Launch all tasks asynchronously and get their Deferred handles. + val jobs = + items.map { item -> + async { + runRequestFor(item) + } + } + + // Use withTimeout to impose a 15-second limit on waiting for all deferreds. + // If all tasks complete within 15 seconds, awaitAll() will return their results, + // and this block will return those results. + withTimeoutOrNull(timeoutMillis) { + jobs.joinAll() + } + + async { + jobs.forEach { + it.cancel("Timeout") + } + } + + jobs.mapNotNull { + if (it.isCompleted) { + it.getCompleted() + } else { + null + } + } + } } From accc8d4439bff5c9058a361ee243f6b4c7f158d1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 9 Jul 2025 16:57:26 -0400 Subject: [PATCH 064/185] Brute forces the finding of events. --- .../amethyst/model/LocalCache.kt | 3 +- .../event/EventFinderFilterAssembler.kt | 2 + .../EventFinderFilterAssemblerSubscription.kt | 8 +- .../loaders/FilterMissingAddressables.kt | 117 +++++++++++++----- .../event/loaders/FilterMissingEvents.kt | 105 ++++++++++++---- .../user/loaders/FilterUserMetadataForKey.kt | 40 +++--- .../user/loaders/UserLoaderSubAssembler.kt | 7 +- .../subassemblies/FilterByAddress.kt | 35 ++++-- .../subassemblies/FilterByAuthor.kt | 6 +- .../subassemblies/FilterByEvent.kt | 35 +++++- .../SearchWatcherSubAssembler.kt | 18 +-- .../loggedIn/threadview/ThreadScreen.kt | 2 +- .../datasources/ThreadFilterAssembler.kt | 2 + .../ThreadFilterAssemblerSubscription.kt | 15 ++- .../FilterMissingEventsForThread.kt | 64 ++++++---- .../ThreadEventLoaderSubAssembler.kt | 3 +- .../quartz/nip01Core/relay/filters/Filter.kt | 28 ++--- 17 files changed, 345 insertions(+), 145 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 596f669237..632be4fdb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -95,6 +95,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.Entity import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay @@ -2928,7 +2929,7 @@ object LocalCache : ILocalCache { } getOrCreateUser(nip19.hex) } - is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> { + is NNote -> { getOrCreateNote(nip19.hex) } is NEvent -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt index beaf7d2511..c9a9685cdd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssembler.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.NoteEventLoaderSubAssembler @@ -29,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to tags, even the same tag class EventFinderQueryState( val note: Note, + val account: Account, ) class EventFinderFilterAssembler( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt index 938c514ead..d0ffe095e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventFinderFilterAssemblerSubscription.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -30,18 +31,19 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel fun EventFinderFilterAssemblerSubscription( note: Note, accountViewModel: AccountViewModel, -) = EventFinderFilterAssemblerSubscription(note, accountViewModel.dataSources().eventFinder) +) = EventFinderFilterAssemblerSubscription(note, accountViewModel.account, accountViewModel.dataSources().eventFinder) @Composable fun EventFinderFilterAssemblerSubscription( note: Note, + account: Account, dataSource: EventFinderFilterAssembler, ) { // different screens get different states // even if they are tracking the same tag. val state = - remember(note) { - EventFinderQueryState(note) + remember(note, account) { + EventFinderQueryState(note, account) } KeyDataSourceSubscription(state, dataSource) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt index c29bdaeb40..bcad63005e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt @@ -22,64 +22,113 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.LocalCache.relayHints import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import com.vitorpamplona.quartz.utils.mapOfSet -fun filterMissingAddressables(keys: List): List? { - val missingAddressables = mutableSetOf
() +fun potentialRelaysToFindAddress(note: AddressableNote): Set { + val set = mutableSetOf() - keys.forEach { - if (it.note is AddressableNote && it.note.event == null) { - missingAddressables.add(it.note.address()) - } + set.addAll(LocalCache.relayHints.hintsForAddress(note.idHex)) - // loads threading that is event-based - it.note.replyTo?.forEach { - if (it is AddressableNote && it.event == null) { - missingAddressables.add(it.address()) - } + val isInChannel = note.channelHex() + if (isInChannel != null) { + LocalCache.checkGetOrCreateChannel(isInChannel)?.relays()?.forEach { + set.add(it) } } - return filterMissingAddressables(missingAddressables) + note.replyTo?.map { parentNote -> + set.addAll(parentNote.relays) + + parentNote + .channelHex() + ?.let { LocalCache.checkGetOrCreateChannel(it) } + ?.relays() + ?.forEach { set.add(it) } + + parentNote.author?.inboxRelays()?.let { set.addAll(it) } + } + + note.replies.map { childNote -> + set.addAll(childNote.relays) + + childNote + .channelHex() + ?.let { LocalCache.checkGetOrCreateChannel(it) } + ?.relays() + ?.forEach { set.add(it) } + + childNote.author?.outboxRelays()?.let { set.addAll(it) } + } + + note.reactions.map { reactionType -> + reactionType.value.forEach { childNote -> + set.addAll(childNote.relays) + childNote.author?.outboxRelays()?.let { set.addAll(it) } + } + } + + note.boosts.map { childNote -> + set.addAll(childNote.relays) + childNote.author?.outboxRelays()?.let { set.addAll(it) } + } + + return set } -fun filterMissingAddressables(missingAddressables: Set
): List { +fun filterMissingAddressables(keys: List): List? { + val addressesPerRelay = + mapOfSet { + keys.forEach { key -> + val default = key.account.followPlusAllMine.flow.value + if (key.note is AddressableNote && key.note.event == null) { + potentialRelaysToFindAddress(key.note).ifEmpty { default }.forEach { relayUrl -> + add(relayUrl, key.note.address) + } + } + + // loads threading that is event-based + key.note.replyTo?.forEach { note -> + if (note is AddressableNote && note.event == null) { + potentialRelaysToFindAddress(note).ifEmpty { default }.forEach { relayUrl -> + add(relayUrl, note.address) + } + } + } + } + } + + return filterMissingAddressables(addressesPerRelay) +} + +fun filterMissingAddressables(missingAddressables: Map>): List { if (missingAddressables.isEmpty()) return emptyList() - return missingAddressables.flatMap { aTag -> - val authorHomeRelayEventAddress = AdvertisedRelayListEvent.createAddressTag(aTag.pubKeyHex) - val authorHomeRelayEvent = (LocalCache.getAddressableNoteIfExists(authorHomeRelayEventAddress)?.event as? AdvertisedRelayListEvent) - - val authorHomeRelays = - authorHomeRelayEvent?.writeRelaysNorm()?.ifEmpty { null } - ?: LocalCache.relayHints.hintsForKey(aTag.pubKeyHex).ifEmpty { null } - ?: listOfNotNull(LocalCache.getUserIfExists(aTag.pubKeyHex)?.latestMetadataRelay) - - val relayHints = LocalCache.relayHints.hintsForAddress(aTag.toValue()) - - (authorHomeRelays + relayHints).toSet().map { - if (aTag.kind < 25000 && aTag.dTag.isBlank()) { + return missingAddressables.flatMap { relayEntry -> + relayEntry.value.map { address -> + if (address.kind < 25000 && address.dTag.isBlank()) { RelayBasedFilter( - relay = it, + relay = relayEntry.key, filter = Filter( - kinds = listOf(aTag.kind), - authors = listOf(aTag.pubKeyHex), + kinds = listOf(address.kind), + authors = listOf(address.pubKeyHex), limit = 1, ), ) } else { RelayBasedFilter( - relay = it, + relay = relayEntry.key, filter = Filter( - kinds = listOf(aTag.kind), - tags = mapOf("d" to listOf(aTag.dTag)), - authors = listOf(aTag.pubKeyHex), + kinds = listOf(address.kind), + tags = mapOf("d" to listOf(address.dTag)), + authors = listOf(address.pubKeyHex), limit = 1, ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt index ced79d5a0b..d526f358bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt @@ -22,48 +22,101 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCache.relayHints +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState -import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet -fun filterMissingEvents(keys: List): List? { - val missingEvents = mutableSetOf() +fun potentialRelaysToFindEvent(note: Note): Set { + val set = mutableSetOf() - keys.forEach { - if (it.note !is AddressableNote && it.note.event == null) { - missingEvents.add(it.note.idHex) - } + set.addAll(LocalCache.relayHints.hintsForEvent(note.idHex)) - // loads threading that is event-based - it.note.replyTo?.forEach { - if (it !is AddressableNote && it.event == null) { - missingEvents.add(it.idHex) - } + val isInChannel = note.channelHex() + if (isInChannel != null) { + LocalCache.checkGetOrCreateChannel(isInChannel)?.relays()?.forEach { + set.add(it) } } - return filterMissingEvents(missingEvents) + note.replyTo?.map { parentNote -> + set.addAll(parentNote.relays) + + parentNote + .channelHex() + ?.let { LocalCache.checkGetOrCreateChannel(it) } + ?.relays() + ?.forEach { set.add(it) } + + parentNote.author?.inboxRelays()?.let { set.addAll(it) } + } + + note.replies.map { childNote -> + set.addAll(childNote.relays) + + childNote + .channelHex() + ?.let { LocalCache.checkGetOrCreateChannel(it) } + ?.relays() + ?.forEach { set.add(it) } + + childNote.author?.outboxRelays()?.let { set.addAll(it) } + } + + note.reactions.map { reactionType -> + reactionType.value.forEach { childNote -> + set.addAll(childNote.relays) + childNote.author?.outboxRelays()?.let { set.addAll(it) } + } + } + + note.boosts.map { childNote -> + set.addAll(childNote.relays) + childNote.author?.outboxRelays()?.let { set.addAll(it) } + } + + return set } -fun filterMissingEvents(missingEventIds: Set): List { - if (missingEventIds.isEmpty()) return emptyList() - - val relayHints = +fun filterMissingEvents(keys: List): List? { + val eventsPerRelay = mapOfSet { - missingEventIds.forEach { eventId -> - LocalCache.relayHints.hintsForEvent(eventId).forEach { relayUrl -> - add(relayUrl, eventId) + keys.forEach { key -> + val default = key.account.followPlusAllMine.flow.value + + if (key.note !is AddressableNote && key.note.event == null) { + potentialRelaysToFindEvent(key.note).ifEmpty { default }.forEach { relayUrl -> + add(relayUrl, key.note.idHex) + } + } + + // loads threading that is event-based + key.note.replyTo?.forEach { note -> + if (note !is AddressableNote && note.event == null) { + potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl -> + add(relayUrl, note.idHex) + } + } } } } - return relayHints.map { - RelayBasedFilter( - relay = it.key, - filter = Filter(ids = it.value.sorted()), - ) + return filterMissingEvents(eventsPerRelay) +} + +fun filterMissingEvents(missingEventIds: Map>): List { + if (missingEventIds.isEmpty()) return emptyList() + + return missingEventIds.mapNotNull { + if (it.value.isNotEmpty()) { + RelayBasedFilter( + relay = it.key, + filter = Filter(ids = it.value.sorted()), + ) + } else { + null + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt index 7ce042af79..b2b659438e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.mapOfSet import kotlin.collections.forEach @@ -36,35 +37,44 @@ val MetadataAndRelayListKinds = AdvertisedRelayListEvent.KIND, ) -fun filterFindUserMetadataForKey(author: HexKey): List = +fun filterFindUserMetadataForKey( + author: HexKey, + defaultRelays: Set, +): List = LocalCache.checkGetOrCreateUser(author)?.let { - filterFindUserMetadataForKey(setOf(it)) + filterFindUserMetadataForKey(setOf(it), defaultRelays) } ?: emptyList() -fun filterFindUserMetadataForKey(authors: Set): List { +fun filterFindUserMetadataForKey( + authors: Set, + defaultRelays: Set, +): List { val perRelayKeys = mapOfSet { authors.forEach { key -> val relays = - key.authorRelayList()?.writeRelaysNorm() + key.authorRelayList()?.writeRelaysNorm()?.ifEmpty { null } ?: LocalCache.relayHints.hintsForKey(key.pubkeyHex).ifEmpty { null } - ?: key.relaysBeingUsed.keys + ?: (key.relaysBeingUsed.keys + defaultRelays).toList() relays.forEach { add(it, key.pubkeyHex) } - // TODO, DESPERATION: TRY ALL THE CONNECTED RELAYS. } } - return perRelayKeys.map { - RelayBasedFilter( - relay = it.key, - filter = - Filter( - kinds = MetadataAndRelayListKinds, - authors = it.value.sorted(), - ), - ) + return perRelayKeys.mapNotNull { + if (it.value.isNotEmpty()) { + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = MetadataAndRelayListKinds, + authors = it.value.sorted(), + ), + ) + } else { + null + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt index 019847c5a4..83c8181260 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/UserLoaderSubAssembler.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubNoEo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class UserLoaderSubAssembler( client: NostrClient, @@ -42,7 +43,11 @@ class UserLoaderSubAssembler( } } + val defaultRelays = mutableSetOf() + keys.mapTo(mutableSetOf()) { it.account }.forEach { + defaultRelays.addAll(it.followPlusAllMine.flow.value) + it.kind3FollowList.flow.value.authors.forEach { val user = LocalCache.getOrCreateUser(it) if (user.latestMetadata == null) { @@ -55,7 +60,7 @@ class UserLoaderSubAssembler( if (firstTimers.isEmpty()) return null - return filterFindUserMetadataForKey(firstTimers) + return filterFindUserMetadataForKey(firstTimers, defaultRelays) } override fun distinct(key: UserFinderQueryState) = key.user diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt index db63d07bbe..7e7320fcbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt @@ -20,17 +20,30 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindAddress +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.utils.mapOfSet +import kotlin.collections.forEach +import kotlin.collections.ifEmpty -fun filterByAddress(address: NAddress) = - filterMissingAddressables( - setOf( - Address( - address.kind, - address.author, - address.dTag, - ), - ), - ) +fun filterByAddress( + address: NAddress, + default: Set, +): List { + val note = LocalCache.getOrCreateAddressableNote(address.address()) + + val list = + mapOfSet { + if (note.event == null) { + potentialRelaysToFindAddress(note).ifEmpty { default }.forEach { relayUrl -> + add(relayUrl, note.address) + } + } + } + + return filterMissingAddressables(list) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt index 4be93be019..c3b2efd28a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAuthor.kt @@ -22,5 +22,9 @@ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassembli import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.filterFindUserMetadataForKey import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -fun filterByAuthor(pubKey: HexKey) = filterFindUserMetadataForKey(pubKey) +fun filterByAuthor( + pubKey: HexKey, + defaultRelays: Set, +) = filterFindUserMetadataForKey(pubKey, defaultRelays) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt index 950b392ae4..e69b20dad2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt @@ -20,7 +20,40 @@ */ package com.vitorpamplona.amethyst.service.relayClient.searchCommand.subassemblies +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.mapOfSet +import kotlin.collections.forEach +import kotlin.collections.ifEmpty -fun filterByEvent(eventId: HexKey) = filterMissingEvents(setOf(eventId)) +fun filterByEvent( + eventId: HexKey, + default: Set, +): List { + val note = LocalCache.checkGetOrCreateNote(eventId) ?: return emptyList() + + val list = + mapOfSet { + if (note !is AddressableNote && note.event == null) { + potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl -> + add(relayUrl, note.idHex) + } + } + + // loads threading that is event-based + note.replyTo?.forEach { parentNote -> + if (parentNote !is AddressableNote && note.event == null) { + potentialRelaysToFindEvent(note).ifEmpty { default }.forEach { relayUrl -> + add(relayUrl, note.idHex) + } + } + } + } + + return filterMissingEvents(list) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt index 39b58880b8..02c29f6713 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt @@ -55,25 +55,27 @@ class SearchWatcherSubAssembler( if (mySearchString.isBlank()) return null + val defaultRelays = key.account.followPlusAllMine.flow.value + val directFilters = runCatching { if (Hex.isHex(mySearchString)) { - val key = Hex.decode(mySearchString).toHexKey() - filterByAuthor(key) + filterByEvent(key) + val hexKey = Hex.decode(mySearchString).toHexKey() + filterByAuthor(hexKey, defaultRelays) + filterByEvent(hexKey, defaultRelays) } else { val parsed = Nip19Parser.uriToRoute(mySearchString)?.entity if (parsed != null) { cache.consume(parsed) when (parsed) { - is NSec -> filterByAuthor(parsed.toPubKeyHex()) - is NPub -> filterByAuthor(parsed.hex) - is NProfile -> filterByAuthor(parsed.hex) - is NNote -> filterByEvent(parsed.hex) - is NEvent -> filterByEvent(parsed.hex) + is NSec -> filterByAuthor(parsed.toPubKeyHex(), defaultRelays) + is NPub -> filterByAuthor(parsed.hex, defaultRelays) + is NProfile -> filterByAuthor(parsed.hex, defaultRelays) + is NNote -> filterByEvent(parsed.hex, defaultRelays) + is NEvent -> filterByEvent(parsed.hex, defaultRelays) is NEmbed -> emptyList() is NRelay -> emptyList() - is NAddress -> filterByAddress(parsed) + is NAddress -> filterByAddress(parsed, defaultRelays) else -> emptyList() } } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt index 5f2158f6c4..5ba92fc7af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt @@ -53,7 +53,7 @@ fun ThreadScreen( ) WatchLifecycleAndUpdateModel(feedViewModel) - ThreadFilterAssemblerSubscription(noteId, accountViewModel.dataSources().thread) + ThreadFilterAssemblerSubscription(noteId, accountViewModel) LoadNote(noteId, accountViewModel) { if (it != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt index 59659205f0..1ac82c64e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssembler.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadEventLoaderSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.subassembies.ThreadFilterSubAssembler @@ -29,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to tags, even the same tag class ThreadQueryState( val eventId: HexKey, + val account: Account, ) class ThreadFilterAssembler( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt index c0407747b8..7954bda887 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/ThreadFilterAssemblerSubscription.kt @@ -22,19 +22,32 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.HexKey @Composable fun ThreadFilterAssemblerSubscription( eventId: HexKey, + accountViewModel: AccountViewModel, +) = ThreadFilterAssemblerSubscription( + eventId, + accountViewModel.account, + accountViewModel.dataSources().thread, +) + +@Composable +fun ThreadFilterAssemblerSubscription( + eventId: HexKey, + account: Account, filterAssembler: ThreadFilterAssembler, ) { // different screens get different states // even if they are tracking the same tag. val state = remember(eventId) { - ThreadQueryState(eventId) + ThreadQueryState(eventId, account) } KeyDataSourceSubscription(state, filterAssembler) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt index 2a77451434..791cadcdfc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt @@ -24,34 +24,54 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingAddressables import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.filterMissingEvents -import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindAddress +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.potentialRelaysToFindEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.mapOfSet +import kotlin.collections.forEach +import kotlin.collections.ifEmpty -fun filterMissingEventsForThread(threadInfo: ThreadAssembler.ThreadInfo): List { - val missingEvents = mutableSetOf() - val missingAddresses = mutableSetOf
() +fun filterMissingEventsForThread( + threadInfo: ThreadAssembler.ThreadInfo, + defaultRelays: Set, +): List { + val missingEvents = + mapOfSet { + if (threadInfo.root.event == null && threadInfo.root !is AddressableNote) { + potentialRelaysToFindEvent(threadInfo.root).ifEmpty { defaultRelays }.forEach { relayUrl -> + add(relayUrl, threadInfo.root.idHex) + } + } - if (threadInfo.root.event == null) { - if (threadInfo.root is AddressableNote) { - missingAddresses.add(threadInfo.root.address) - } else { - missingEvents.add(threadInfo.root.idHex) - } - } - - threadInfo.allNotes.forEach { - if (it.event == null) { - if (it is AddressableNote) { - missingAddresses.add(it.address) - } else { - missingEvents.add(it.idHex) + threadInfo.allNotes.forEach { + if (it !is AddressableNote && it.event == null) { + potentialRelaysToFindEvent(it).ifEmpty { defaultRelays }.forEach { relayUrl -> + add(relayUrl, it.idHex) + } + } } } - } - val missingEventsFilter = filterMissingEvents(missingEvents) ?: emptyList() - val missingAddressFilter = filterMissingAddressables(missingAddresses) ?: emptyList() + val missingAddresses = + mapOfSet { + if (threadInfo.root.event == null && threadInfo.root is AddressableNote) { + potentialRelaysToFindEvent(threadInfo.root).ifEmpty { defaultRelays }.forEach { relayUrl -> + add(relayUrl, threadInfo.root.address) + } + } + + threadInfo.allNotes.forEach { + if (it is AddressableNote && it.event == null) { + potentialRelaysToFindAddress(it).ifEmpty { defaultRelays }.forEach { relayUrl -> + add(relayUrl, it.address) + } + } + } + } + + val missingEventsFilter = filterMissingEvents(missingEvents) + val missingAddressFilter = filterMissingAddressables(missingAddresses) return missingEventsFilter + missingAddressFilter } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt index c6703e69de..5018c949a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt @@ -45,7 +45,8 @@ class ThreadEventLoaderSubAssembler( since: SincePerRelayMap?, ): List? { val branches = ThreadAssembler().findThreadFor(key.eventId) ?: return null - return filterMissingEventsForThread(branches) + val defaultRelays = key.account.followPlusAllMine.flow.value + return filterMissingEventsForThread(branches, defaultRelays) } override fun id(key: ThreadQueryState) = key.eventId diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index 5472bb9c0d..90e5d83562 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.nip01Core.relay.filters import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address class Filter( val ids: List? = null, @@ -48,22 +47,13 @@ class Filter( search: String? = this.search, ) = Filter(ids, authors, kinds, tags, since, until, limit, search) - init { - // TODO: Remove this in production - ids?.forEach { - if (it.length != 64) throw IllegalArgumentException("Invalid id length $it on ${toJson()}") - } - authors?.forEach { - if (it.length != 64) throw IllegalArgumentException("Invalid author length $it on ${toJson()}") - } - tags?.get("#p")?.forEach { - if (it.length != 64) throw IllegalArgumentException("Invalid p-tag length $it on ${toJson()}") - } - tags?.get("#e")?.forEach { - if (it.length != 64) throw IllegalArgumentException("Invalid e-tag length $it on ${toJson()}") - } - tags?.get("#a")?.forEach { - if (Address.parse(it) != null) throw IllegalArgumentException("Invalid a-tag length $it on ${toJson()}") - } - } + fun isFilledFilter() = + (ids != null && ids.isNotEmpty()) || + (authors != null && authors.isNotEmpty()) || + (kinds != null && kinds.isNotEmpty()) || + (tags != null && tags.isNotEmpty()) || + (since != null) || + (until != null) || + (limit != null) || + (search != null && search.isNotEmpty()) } From 35005dddad75f3a26b0a6ee7bb923ca220d4d4a2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 9 Jul 2025 17:20:59 -0400 Subject: [PATCH 065/185] Adds checks in the filter to highlight issues with empty or invalid filters --- .../relay/client/pool/RelayBasedFilter.kt | 19 +++++-------- .../quartz/nip01Core/relay/filters/Filter.kt | 27 ++++++++++++++++++- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt index b5ebfe3583..ed0fcfa9c1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayBasedFilter.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.client.pool +import android.util.Log import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -29,22 +30,16 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class RelayBasedFilter( val relay: NormalizedRelayUrl, val filter: Filter, -) { - // This only exists because some relays confuse empty lists with null lists - fun isValidFor(relay: NormalizedRelayUrl) = relay == this.relay - - fun toFilter(relay: NormalizedRelayUrl): Filter? = - if (isValidFor(relay)) { - filter - } else { - null - } -} +) fun List.groupByRelay(): Map> { val result = mutableMapOf>() for (relayBasedFilter in this) { - result.getOrPut(relayBasedFilter.relay) { mutableListOf() }.add(relayBasedFilter.filter) + if (relayBasedFilter.filter.isFilledFilter()) { + result.getOrPut(relayBasedFilter.relay) { mutableListOf() }.add(relayBasedFilter.filter) + } else { + Log.e("FilterError", "Ignoring empty filter for ${relayBasedFilter.relay}") + } } return result } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index 90e5d83562..06bea99c58 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.filters +import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address class Filter( val ids: List? = null, @@ -51,9 +53,32 @@ class Filter( (ids != null && ids.isNotEmpty()) || (authors != null && authors.isNotEmpty()) || (kinds != null && kinds.isNotEmpty()) || - (tags != null && tags.isNotEmpty()) || + (tags != null && tags.isNotEmpty() && tags.values.all { it.isNotEmpty() }) || (since != null) || (until != null) || (limit != null) || (search != null && search.isNotEmpty()) + + init { + if (!isFilledFilter()) { + Log.e("FilterError", "Filter is empty: ${toJson()}") + } + + ids?.forEach { + if (it.length != 64) Log.e("FilterError", "Invalid id length $it on ${toJson()}") + } + authors?.forEach { + if (it.length != 64) Log.e("FilterError", "Invalid author length $it on ${toJson()}") + } + // tests common tags. + tags?.get("p")?.forEach { + if (it.length != 64) Log.e("FilterError", "Invalid p-tag length $it on ${toJson()}") + } + tags?.get("e")?.forEach { + if (it.length != 64) Log.e("FilterError", "Invalid e-tag length $it on ${toJson()}") + } + tags?.get("a")?.forEach { + if (Address.parse(it) == null) Log.e("FilterError", "Invalid a-tag $it on ${toJson()}") + } + } } From 123803edd3128423dd6b1d4208faadfadfeb6b64 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 9 Jul 2025 17:48:19 -0400 Subject: [PATCH 066/185] Fixes reply count --- .../relayClient/reqCommand/event/EventObservers.kt | 8 ++++---- .../service/relayClient/reqCommand/user/UserObservers.kt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt index ba74756058..960018f896 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/EventObservers.kt @@ -176,9 +176,9 @@ fun observeNoteReplyCount( remember(note) { note .flow() - .reactions.stateFlow + .replies.stateFlow + .sample(200) .mapLatest { it.note.replies.size } - .sample(1000) .distinctUntilChanged() } @@ -215,8 +215,8 @@ fun observeNoteReactionCount( note .flow() .reactions.stateFlow + .sample(200) .mapLatest { it.note.countReactions() } - .sample(1000) .distinctUntilChanged() .flowOn(Dispatchers.Default) } @@ -294,7 +294,7 @@ fun observeNoteRepostCount( note .flow() .boosts.stateFlow - .sample(1000) + .sample(200) .mapLatest { note.boosts.size } .distinctUntilChanged() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 2848f1d833..23ef0ae476 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -249,7 +249,7 @@ fun observeUserFollowCount( user .flow() .followers.stateFlow - .sample(1000) + .sample(200) .mapLatest { userState -> userState.user.transientFollowCount() ?: 0 }.distinctUntilChanged() @@ -303,7 +303,7 @@ fun observeUserTagFollows( .hashtagFollows(user) .flow() .metadata.stateFlow - .sample(1000) + .sample(200) .mapLatest { noteState -> (noteState.note.event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.sorted() ?: emptyList() }.onStart { @@ -345,7 +345,7 @@ fun observeUserBookmarkCount( user .flow() .followers.stateFlow - .sample(1000) + .sample(200) .mapLatest { userState -> userState.user.latestBookmarkList?.countBookmarks() ?: 0 }.distinctUntilChanged() @@ -385,7 +385,7 @@ fun observeUserFollowerCount( user .flow() .followers.stateFlow - .sample(1000) + .sample(200) .mapLatest { userState -> userState.user.transientFollowerCount() }.distinctUntilChanged() From a2d16c173c70ff58b12c4258228bfeb6ab6dc2af Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 09:38:53 -0400 Subject: [PATCH 067/185] Fixing crash when the last `:` is not present in an address --- .../quartz/nip01Core/tags/addressables/Address.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt index 9ac889a43e..93b2d48c09 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt @@ -66,8 +66,8 @@ data class Address( fun parse(addressId: String): Address? = try { val parts = addressId.split(":", limit = 3) - if (parts.size > 1 && parts[1].length == 64 && Hex.isHex(parts[1])) { - Address(parts[0].toInt(), parts[1], parts[2]) + if (parts.size > 2 && parts[1].length == 64 && Hex.isHex(parts[1])) { + Address(parts[0].toInt(), parts[1], parts.getOrNull(2) ?: "") } else { if (addressId.startsWith("naddr1")) { val addr = Nip19Parser.uriToRoute(addressId)?.entity From ec985d58e74f2dea596e1dcf908c3ea1dd6475a6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 09:43:36 -0400 Subject: [PATCH 068/185] Fixes matching image borders in the loading step of preview cards --- .../amethyst/ui/components/MyAsyncImage.kt | 13 ++++++++++++- .../amethyst/ui/note/DisplayAuthorBanner.kt | 9 +++------ .../ui/note/elements/DefaultImageHeader.kt | 10 +++++----- .../amethyst/ui/note/types/Classifieds.kt | 3 ++- .../amethyst/ui/note/types/FollowList.kt | 5 +++-- .../amethyst/ui/note/types/Highlight.kt | 4 ++-- .../amethyst/ui/note/types/LiveActivity.kt | 2 +- .../amethyst/ui/note/types/LongForm.kt | 5 +++-- .../discover/nip51FollowSets/FollowSetCard.kt | 14 ++++---------- .../discover/nip99Classifieds/ClassifiedsThumb.kt | 2 +- 10 files changed, 36 insertions(+), 31 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt index 398a4f4be1..3d35c44631 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt @@ -90,7 +90,18 @@ fun MyAsyncImage( } is AsyncImagePainter.State.Error -> { if (onError != null) { - onError() + if (ratio != null) { + Box(loadedImageModifier.aspectRatio(ratio), contentAlignment = Alignment.Center) { + onError() + } + } else { + Box(loadedImageModifier, contentAlignment = Alignment.Center) { + if (onLoadingBackground != null) { + onLoadingBackground() + } + DisplayUrlWithLoadingSymbol(imageUrl) + } + } } } is AsyncImagePainter.State.Success -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt index 4ecc152d94..81de3a9ea1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt @@ -20,26 +20,23 @@ */ package com.vitorpamplona.amethyst.ui.note -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.SimpleImageBorder @Composable fun DisplayAuthorBanner( note: Note, accountViewModel: AccountViewModel, + modifier: Modifier = SimpleImageBorder, ) { WatchAuthor(note, accountViewModel) { BannerImage( it, - Modifier - .fillMaxSize() - .clip(QuoteBorder), + modifier, accountViewModel, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt index 62bca2ccef..5d0c30fdc2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt @@ -22,15 +22,12 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.blur import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -41,6 +38,7 @@ import com.vitorpamplona.amethyst.ui.note.WatchAuthor import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.SimpleHeaderImage import com.vitorpamplona.amethyst.ui.theme.Size16dp import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.authorNotePictureForImageHeader @@ -49,10 +47,11 @@ import com.vitorpamplona.amethyst.ui.theme.authorNotePictureForImageHeader fun DefaultImageHeader( note: Note, accountViewModel: AccountViewModel, + modifier: Modifier = SimpleHeaderImage, ) { WatchAuthor(baseNote = note, accountViewModel) { Box { - BannerImage(it, Modifier.fillMaxWidth().heightIn(max = 200.dp), accountViewModel) + BannerImage(it, modifier, accountViewModel) Box(authorNotePictureForImageHeader.align(Alignment.BottomStart)) { BaseUserPicture(it, Size55dp, accountViewModel, Modifier) @@ -65,10 +64,11 @@ fun DefaultImageHeader( fun DefaultImageHeaderBackground( note: Note, accountViewModel: AccountViewModel, + modifier: Modifier = SimpleHeaderImage, ) { WatchAuthor(baseNote = note, accountViewModel) { Box { - BannerImage(it, Modifier.fillMaxWidth().heightIn(max = 200.dp).blur(Size16dp), accountViewModel) + BannerImage(it, modifier.blur(Size16dp), accountViewModel) Box(authorNotePictureForImageHeader.align(Alignment.BottomStart)) { BaseUserPicture(it, Size55dp, accountViewModel, Modifier) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt index 55720df0b4..0afc218885 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -99,7 +100,7 @@ fun RenderClassifieds( ) } } ?: run { - DefaultImageHeader(note, accountViewModel) + DefaultImageHeader(note, accountViewModel, Modifier.fillMaxWidth()) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt index de9f74717c..4f8a0c237f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import kotlinx.collections.immutable.ImmutableList @@ -97,13 +98,13 @@ fun DisplayFollowList( ), contentScale = ContentScale.Crop, mainImageModifier = Modifier.fillMaxWidth(), - loadedImageModifier = Modifier, + loadedImageModifier = FollowSetImageModifier, accountViewModel = accountViewModel, onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) }, onError = { DefaultImageHeader(baseNote, accountViewModel) }, ) } ?: run { - DefaultImageHeader(baseNote, accountViewModel) + DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier) } Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index 828c6cbfe9..67d96b491a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -262,7 +262,7 @@ fun DisplayEntryForNote( RenderUserAsClickableText(author, null, accountViewModel, nav) } - val noteEvent = noteState?.note?.event as? BaseThreadedEvent ?: return + val noteEvent = noteState.note.event as? BaseThreadedEvent ?: return val description = remember(noteEvent) { noteEvent.tags.firstTagValueFor("title", "subject", "alt") } @@ -274,7 +274,7 @@ fun DisplayEntryForNote( onClick = { routeFor(note, accountViewModel.userProfile())?.let { nav.nav(it) } }, ) } else { - DisplayEvent(noteEvent.id, noteEvent.kind, note.toNostrUri(), null, accountViewModel, nav) + DisplayEvent(noteEvent.id, note.toNostrUri(), null, accountViewModel, nav) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index d81c3fdd38..80063464de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -237,7 +237,7 @@ fun RenderLiveActivityEventInner( onError = { DefaultImageHeader(baseNote, accountViewModel) }, ) } - } ?: run { DisplayAuthorBanner(baseNote, accountViewModel) } + } ?: run { DisplayAuthorBanner(baseNote, accountViewModel, MaterialTheme.colorScheme.imageModifier) } Text( text = stringRes(id = R.string.live_stream_has_ended), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt index 3defcc5679..d029ba7b68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -44,6 +44,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.SimpleImageBorder import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.subtleBorder @@ -93,14 +94,14 @@ fun LongFormHeader( it, ), contentScale = ContentScale.FillWidth, - mainImageModifier = Modifier.fillMaxWidth(), + mainImageModifier = SimpleImageBorder, loadedImageModifier = Modifier, accountViewModel = accountViewModel, onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel) }, onError = { DefaultImageHeader(note, accountViewModel) }, ) } ?: run { - DefaultImageHeader(note, accountViewModel) + DefaultImageHeader(note, accountViewModel, SimpleImageBorder) } title?.let { Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt index 034a455512..e26262a4cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme @@ -35,7 +34,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -58,7 +56,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size25dp @@ -158,17 +156,13 @@ fun RenderFollowSetThumb( it, ), contentScale = ContentScale.Crop, - mainImageModifier = Modifier.fillMaxWidth(), - loadedImageModifier = - Modifier - .fillMaxWidth() - .aspectRatio(ratio = 21f / 9f) - .clip(QuoteBorder), + mainImageModifier = Modifier, + loadedImageModifier = FollowSetImageModifier, accountViewModel = accountViewModel, onLoadingBackground = { DefaultImageHeaderBackground(baseNote, accountViewModel) }, onError = { DefaultImageHeader(baseNote, accountViewModel) }, ) - } ?: run { DefaultImageHeader(baseNote, accountViewModel) } + } ?: run { DefaultImageHeader(baseNote, accountViewModel, FollowSetImageModifier) } Gallery(card.users, Modifier.padding(Size10dp), accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt index c607e9b296..ee84f3d0a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt @@ -118,7 +118,7 @@ fun InnerRenderClassifiedsThumb( contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize(), ) - } ?: run { DisplayAuthorBanner(note, accountViewModel) } + } ?: run { DisplayAuthorBanner(note, accountViewModel, Modifier.fillMaxSize()) } Row( Modifier From f9189e9ec137bea6995cd96d6a489534da4373ae Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 10:57:42 -0400 Subject: [PATCH 069/185] Further separates channels by kind in the UI --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../vitorpamplona/amethyst/model/Channel.kt | 43 ++--- .../amethyst/model/LocalCache.kt | 61 ++++--- .../com/vitorpamplona/amethyst/model/Note.kt | 6 +- .../amethyst/model/ParticipantListBuilder.kt | 2 +- .../com/vitorpamplona/amethyst/model/User.kt | 4 +- .../eoseManagers/PerUniqueIdEoseManager.kt | 14 +- .../PerUserAndFollowListEoseManager.kt | 8 +- ...adataAndLiveActivityWatcherSubAssembler.kt | 2 +- .../ChannelLoaderSubAssembler.kt | 2 +- .../ChannelMetadataWatcherSubAssembler.kt | 5 +- .../LiveActivityWatcherSubAssembly.kt | 5 +- .../SearchWatcherSubAssembler.kt | 4 +- .../amethyst/service/relays/EOSE.kt | 27 ++- .../vitorpamplona/amethyst/ui/MainActivity.kt | 49 +++--- .../amethyst/ui/actions/NewMessageTagger.kt | 1 - .../amethyst/ui/components/ClickableRoute.kt | 79 ++------- .../amethyst/ui/components/RichTextViewer.kt | 6 +- .../amethyst/ui/dal/FeedFilter.kt | 2 +- .../ui/feeds/ChannelFeedContentState.kt | 16 +- .../amethyst/ui/feeds/ChannelFeedState.kt | 4 +- .../amethyst/ui/feeds/FeedContentState.kt | 2 +- .../ui/navigation/AccountSwitchBottomSheet.kt | 4 +- .../amethyst/ui/navigation/AppNavigation.kt | 12 +- .../amethyst/ui/navigation/RouteMaker.kt | 49 +++--- .../amethyst/ui/navigation/Routes.kt | 15 +- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 38 ++--- .../amethyst/ui/note/NoteCompose.kt | 14 +- .../amethyst/ui/note/PubKeyFormatter.kt | 6 +- .../nip22Comments/CommentPostViewModel.kt | 2 +- .../amethyst/ui/note/types/ChannelMessage.kt | 5 +- .../amethyst/ui/note/types/LiveActivity.kt | 13 +- .../ui/note/types/LiveActivityChatMessage.kt | 24 +-- .../ui/screen/loggedIn/AccountViewModel.kt | 17 +- .../loggedIn/chats/feed/ChatMessageCompose.kt | 46 +++-- .../datasource/ChatroomFilterSubAssembler.kt | 2 +- .../privateDM/send/ChatNewMessageViewModel.kt | 4 +- .../publicChannels/dal/ChannelFeedFilter.kt | 2 +- .../ChannelFromUserFilterSubAssembler.kt | 5 +- .../ChannelPublicFilterSubAssembler.kt | 5 +- .../FilterMessagesToLiveStream.kt | 2 +- .../FilterMyMessagesToLiveActivities.kt | 2 +- .../publicChannels/ephemChat/ChannelView.kt | 140 +++++++++++++++ .../EphemeralChatScreen.kt} | 39 +---- .../ephemChat/LoadEphemeralChatChannel.kt | 41 +++++ .../header/ShortEphemeralChatChannelHeader.kt | 2 +- .../{ => nip28PublicChat}/ChannelView.kt | 37 +--- .../PublicChatChannelHeader.kt} | 57 ++----- .../PublicChatChannelScreen.kt | 55 ++++++ .../metadata/ChannelMetadataScreen.kt | 4 +- .../nip53LiveActivities/ChannelView.kt | 142 +++++++++++++++ .../LiveActivitiesChannelHeader.kt | 2 +- .../LiveActivityChannelScreen.kt | 55 ++++++ .../LiveChannelActionOptions.kt | 77 --------- .../LongLiveActivityChannelHeader.kt | 78 +++++---- .../ShortLiveActivityChannelHeader.kt | 90 ++++++++-- .../header/LiveActivityTopBar.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 3 +- .../chats/rooms/ChatroomHeaderCompose.kt | 161 +++++------------- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 4 +- .../datasource/FilterFollowingPublicChats.kt | 2 +- .../FilterLastMessageFollowingPublicChats.kt | 2 +- .../chats/rooms/twopane/MessagesTwoPane.kt | 14 +- .../chats/rooms/twopane/TwoPaneNav.kt | 12 +- .../loggedIn/discover/ChannelCardCompose.kt | 4 +- .../DiscoveryFollowsDiscoverySubAssembler1.kt | 2 +- .../DiscoveryFollowsDiscoverySubAssembler2.kt | 2 +- .../DiscoveryFollowsDiscoverySubAssembler3.kt | 2 +- .../nip28Chats/DiscoverChatFeedFilter.kt | 6 +- ...umb.kt => RenderPublicChatChannelThumb.kt} | 14 +- .../DiscoverLiveFeedFilter.kt | 12 +- .../nip53LiveActivities/LiveActivityCard.kt | 38 +++-- .../nip99Classifieds/NewProductViewModel.kt | 2 +- .../GeoHashFeedFilterSubAssembler.kt | 2 +- .../HashtagFeedFilterSubAssembler.kt | 2 +- .../ui/screen/loggedIn/home/HomeScreen.kt | 7 +- .../loggedIn/home/ShortNotePostViewModel.kt | 3 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 24 ++- .../HomeOutboxEventsEoseManager.kt | 2 +- .../notifications/CardFeedContentState.kt | 2 +- .../loggedIn/redirect/LoadRedirectScreen.kt | 108 ++---------- .../loggedIn/search/SearchBarViewModel.kt | 61 ++++--- .../ui/screen/loggedIn/search/SearchScreen.kt | 65 ++++++- .../loggedIn/threadview/ThreadFeedView.kt | 5 +- .../ThreadEventLoaderSubAssembler.kt | 3 +- .../subassembies/ThreadFilterSubAssembler.kt | 3 +- .../VideoOutboxEventsFilterSubAssembler.kt | 2 +- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 11 ++ .../streaming/LiveActivitiesEvent.kt | 11 ++ .../streaming/tags/StatusTag.kt | 18 ++ 90 files changed, 1167 insertions(+), 874 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/{ChannelScreen.kt => ephemChat/EphemeralChatScreen.kt} (60%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/{ => nip28PublicChat}/ChannelView.kt (83%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/{ChannelHeader.kt => nip28PublicChat/PublicChatChannelHeader.kt} (60%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveChannelActionOptions.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/{RenderChannelThumb.kt => RenderPublicChatChannelThumb.kt} (95%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 0e388f09c7..a673021416 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2254,7 +2254,7 @@ class Account( delay(1000 * 60 * 1) // waits 5 minutes before migrating the list. val contactList = userProfile().latestContactList - val oldChannels = contactList?.taggedEventIds()?.toSet()?.mapNotNull { cache.getChannelIfExists(it) as? PublicChatChannel } + val oldChannels = contactList?.taggedEventIds()?.toSet()?.mapNotNull { cache.getPublicChatChannelIfExists(it) as? PublicChatChannel } if (oldChannels != null && oldChannels.isNotEmpty()) { Log.d("DB UPGRADE", "Migrating List with ${oldChannels.size} old channels ") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index b7ba6f7c7a..7493edddd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder -import com.vitorpamplona.amethyst.ui.note.toShortenHex +import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle @@ -34,23 +34,17 @@ import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip19Bech32.toNEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.LargeCache import kotlinx.coroutines.flow.MutableStateFlow @Stable class EphemeralChatChannel( val roomId: RoomId, -) : Channel(roomId.toKey()) { - override fun idNote() = roomId.toDisplayKey() - - override fun idDisplayNote() = idNote().toShortenHex() - +) : Channel() { override fun relays() = setOf(roomId.relayUrl) override fun toBestDisplayName() = roomId.toDisplayKey() @@ -64,8 +58,8 @@ class EphemeralChatChannel( @Stable class PublicChatChannel( - idHex: String, -) : Channel(idHex) { + val idHex: String, +) : Channel() { var event: ChannelCreateEvent? = null var infoTags = EmptyTagList var info = ChannelDataNorm(null, null, null, null) @@ -110,7 +104,7 @@ class PublicChatChannel( super.updateChannelInfo(creator, updatedAt) } - override fun toBestDisplayName(): String = info.name ?: super.toBestDisplayName() + override fun toBestDisplayName(): String = info.name ?: toNEvent().toShortDisplay() override fun summary(): String? = info.about @@ -119,19 +113,18 @@ class PublicChatChannel( return info.picture ?: super.profilePicture() } - override fun anyNameStartsWith(prefix: String): Boolean = listOfNotNull(info.name, info.about).any { it.contains(prefix, true) } + override fun anyNameStartsWith(prefix: String): Boolean = + idHex.startsWith(prefix) || + info.name?.contains(prefix, true) == true || + info.about?.contains(prefix, true) == true } @Stable class LiveActivitiesChannel( val address: Address, -) : Channel(address.toValue()) { +) : Channel() { var info: LiveActivitiesEvent? = null - override fun idNote() = toNAddr() - - override fun idDisplayNote() = idNote().toShortenHex() - fun address() = address override fun relays() = info?.allRelayUrls()?.toSet() ?: super.relays() @@ -149,7 +142,7 @@ class LiveActivitiesChannel( super.updateChannelInfo(creator, updatedAt) } - override fun toBestDisplayName(): String = info?.title() ?: super.toBestDisplayName() + override fun toBestDisplayName(): String = info?.title() ?: toNAddr().toShortDisplay() override fun summary(): String? = info?.summary() @@ -171,9 +164,7 @@ data class Counter( ) @Stable -abstract class Channel( - val idHex: String, -) { +abstract class Channel { var creator: User? = null var updatedMetadataAt: Long = 0 val notes = LargeCache() @@ -181,11 +172,7 @@ abstract class Channel( private var relays = mapOf() - open fun idNote() = Hex.decode(idHex).toNEvent() - - open fun idDisplayNote() = idNote().toShortenHex() - - open fun toBestDisplayName(): String = idDisplayNote() + abstract fun toBestDisplayName(): String open fun summary(): String? = null @@ -248,10 +235,6 @@ abstract class Channel( notes.remove(note.idHex) } - fun removeNote(noteHex: String) { - notes.remove(noteHex) - } - abstract fun anyNameStartsWith(prefix: String): Boolean fun pruneOldMessages(): Set { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 632be4fdb0..c177f69036 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -89,6 +89,7 @@ import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.decodeEventIdAsHexOrNull import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull import com.vitorpamplona.quartz.nip19Bech32.entities.Entity @@ -320,11 +321,11 @@ object LocalCache : ILocalCache { fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId) - fun getChannelIfExists(key: String): PublicChatChannel? = publicChatChannels.get(key) + fun getPublicChatChannelIfExists(key: String): PublicChatChannel? = publicChatChannels.get(key) - fun getChannelIfExists(key: RoomId): EphemeralChatChannel? = ephemeralChannels.get(key) + fun getEphemeralChatChannelIfExists(key: RoomId): EphemeralChatChannel? = ephemeralChannels.get(key) - fun getChannelIfExists(key: Address): LiveActivitiesChannel? = liveChatChannels.get(key) + fun getLiveActivityChannelIfExists(key: Address): LiveActivitiesChannel? = liveChatChannels.get(key) fun getNoteIfExists(event: Event): Note? = if (event is AddressableEvent) { @@ -1345,10 +1346,10 @@ object LocalCache : ILocalCache { masterNote.removeReport(deleteNote) } - deleteNote.channelHex()?.let { getChannelIfExists(it)?.removeNote(deleteNote) } + deleteNote.channelHex()?.let { getPublicChatChannelIfExists(it)?.removeNote(deleteNote) } (deletedEvent as? LiveActivitiesChatMessageEvent)?.activity()?.let { - getChannelIfExists(it.toTag())?.removeNote(deleteNote) + getPublicChatChannelIfExists(it.toTag())?.removeNote(deleteNote) } (deletedEvent as? TorrentCommentEvent)?.torrentIds()?.let { @@ -2318,31 +2319,45 @@ object LocalCache : ILocalCache { } } - fun findChannelsStartingWith(text: String): List { - checkNotInMainThread() - + fun findPublicChatChannelsStartingWith(text: String): List { if (text.isBlank()) return emptyList() val key = decodeEventIdAsHexOrNull(text) - if (key != null && getChannelIfExists(key) != null) { - return listOfNotNull(getChannelIfExists(key)) + if (key != null) { + getPublicChatChannelIfExists(key)?.let { + return listOf(it) + } } return publicChatChannels.filter { _, channel -> - channel.anyNameStartsWith(text) || - channel.idHex.startsWith(text, true) || - channel.idNote().startsWith(text, true) - } + - ephemeralChannels.filter { _, channel -> - channel.anyNameStartsWith(text) || - channel.idHex.startsWith(text, true) || - channel.idNote().startsWith(text, true) - } + - liveChatChannels.filter { _, channel -> - channel.anyNameStartsWith(text) || - channel.idHex.startsWith(text, true) || - channel.idNote().startsWith(text, true) + channel.anyNameStartsWith(text) + } + } + + fun findEphemeralChatChannelsStartingWith(text: String): List { + if (text.isBlank()) return emptyList() + + return ephemeralChannels.filter { _, channel -> + channel.anyNameStartsWith(text) + } + } + + fun findLiveActivityChannelsStartingWith(text: String): List { + if (text.isBlank()) return emptyList() + + try { + val parsed = Nip19Parser.uriToRoute(text)?.entity + if (parsed is NAddress && parsed.kind == LiveActivitiesEvent.KIND) { + return listOf(getOrCreateLiveChannel(parsed.address())) } + } catch (e: Exception) { + if (e is CancellationException) throw e + null + } + + return liveChatChannels.filter { _, channel -> + channel.anyNameStartsWith(text) + } } suspend fun findStatusesForUser(user: User): ImmutableList { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index eb2bb4f646..267b16318f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.service.replace -import com.vitorpamplona.amethyst.ui.note.toShortenHex +import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent @@ -91,7 +91,7 @@ class AddressableNote( override fun toNEvent() = toNAddr() - override fun idDisplayNote() = idNote().toShortenHex() + override fun idDisplayNote() = idNote().toShortDisplay() override fun address() = address @@ -212,7 +212,7 @@ open class Note( fun toNostrUri(): String = "nostr:${toNEvent()}" - open fun idDisplayNote() = idNote().toShortenHex() + open fun idDisplayNote() = idNote().toShortDisplay() fun channelHex(): HexKey? = if ( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt index 21b644ca45..8e1badf188 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ParticipantListBuilder.kt @@ -96,7 +96,7 @@ class ParticipantListBuilder { it.replyTo?.forEach { addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet) } } - LocalCache.getChannelIfExists(baseNote.idHex)?.notes?.forEach { key, it -> + LocalCache.getPublicChatChannelIfExists(baseNote.idHex)?.notes?.forEach { key, it -> addFollowsThatDirectlyParticipateOnToSet(it, followingSet, mySet) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index c633876cae..2e7b27ece0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.ui.note.toShortenHex +import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.lightning.Lud06 import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent @@ -78,7 +78,7 @@ class User( fun pubkeyNpub() = pubkey().toNpub() - fun pubkeyDisplayHex() = pubkeyNpub().toShortenHex() + fun pubkeyDisplayHex() = pubkeyNpub().toShortDisplay() fun dmInboxRelayList() = (LocalCache.getAddressableNoteIfExists(ChatMessageRelayListEvent.createAddressTag(pubkeyHex))?.event as? ChatMessageRelayListEvent) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt index 045d6c9021..606086a77b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers -import com.vitorpamplona.amethyst.service.relays.EOSEFollowList +import com.vitorpamplona.amethyst.service.relays.EOSEByKey import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient @@ -37,16 +37,16 @@ import kotlin.collections.distinctBy * This class keeps EOSEs for each SubID.id() for as long as possible and * shares all EOSEs among all users. */ -abstract class PerUniqueIdEoseManager( +abstract class PerUniqueIdEoseManager( client: NostrClient, allKeys: () -> Set, val invalidateAfterEose: Boolean = false, ) : BaseEoseManager(client, allKeys) { // long term EOSE cache - private val latestEOSEs = EOSEFollowList() + private val latestEOSEs = EOSEByKey() // map between each query Id and each subscription id - private val userSubscriptionMap = mutableMapOf() + private val userSubscriptionMap = mutableMapOf() fun since(key: T) = latestEOSEs.since(id(key)) @@ -67,7 +67,7 @@ abstract class PerUniqueIdEoseManager( } open fun endSub( - key: String, + key: U, subId: String, ) { dismissSubscription(subId) @@ -87,7 +87,7 @@ abstract class PerUniqueIdEoseManager( override fun updateSubscriptions(keys: Set) { val uniqueSubscribedAccounts = keys.distinctBy { id(it) } - val updated = mutableSetOf() + val updated = mutableSetOf() uniqueSubscribedAccounts.forEach { val mainKey = id(it) @@ -107,5 +107,5 @@ abstract class PerUniqueIdEoseManager( since: SincePerRelayMap?, ): List? - abstract fun id(key: T): String + abstract fun id(key: T): U } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index 26e1a72e6c..6593d6a8ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relays.EOSEAccount +import com.vitorpamplona.amethyst.service.relays.EOSEAccountKey import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient @@ -41,12 +41,12 @@ import kotlin.collections.distinctBy * does NOT share EOSEs with other users. Changing the list will not make the * app reuse the EOSE because it assumes the filter is going to be different */ -abstract class PerUserAndFollowListEoseManager( +abstract class PerUserAndFollowListEoseManager( client: NostrClient, allKeys: () -> Set, val invalidateAfterEose: Boolean = false, ) : BaseEoseManager(client, allKeys) { - private val latestEOSEs = EOSEAccount() + private val latestEOSEs = EOSEAccountKey() private val userSubscriptionMap = mutableMapOf() fun since(key: T) = latestEOSEs.since(user(key), list(key)) @@ -108,5 +108,5 @@ abstract class PerUserAndFollowListEoseManager( abstract fun user(key: T): User - abstract fun list(key: T): String + abstract fun list(key: T): U } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt index b8cb33d36a..e334422a13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt @@ -76,5 +76,5 @@ class ChannelMetadataAndLiveActivityWatcherSubAssembler( } } - override fun distinct(key: ChannelFinderQueryState) = key.channel.idHex + override fun distinct(key: ChannelFinderQueryState) = key.channel } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt index 83ea7d3dee..5931a339c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelLoaderSubAssembler.kt @@ -40,5 +40,5 @@ class ChannelLoaderSubAssembler( ) : SingleSubNoEoseCacheEoseManager(client, allKeys, invalidateAfterEose = true) { override fun updateFilter(keys: List): List? = filterMissingChannelsById(keys) - override fun distinct(key: ChannelFinderQueryState) = key.channel.idHex + override fun distinct(key: ChannelFinderQueryState) = key.channel } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt index b515fba131..8022e81cb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState @@ -30,7 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ChannelMetadataWatcherSubAssembler( client: NostrClient, allKeys: () -> Set, -) : PerUniqueIdEoseManager(client, allKeys) { +) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: ChannelFinderQueryState, since: SincePerRelayMap?, @@ -46,5 +47,5 @@ class ChannelMetadataWatcherSubAssembler( /** * Only one key per channel. */ - override fun id(key: ChannelFinderQueryState) = key.channel.idHex + override fun id(key: ChannelFinderQueryState) = key.channel } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt index 3a5bd44aba..2a64c9c63b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState @@ -34,7 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class LiveActivityWatcherSubAssembly( client: NostrClient, allKeys: () -> Set, -) : PerUniqueIdEoseManager(client, allKeys) { +) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: ChannelFinderQueryState, since: SincePerRelayMap?, @@ -50,5 +51,5 @@ class LiveActivityWatcherSubAssembly( /** * Only one key per channel. */ - override fun id(key: ChannelFinderQueryState) = key.channel.idHex + override fun id(key: ChannelFinderQueryState) = key.channel } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt index 02c29f6713..edd074b732 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchWatcherSubAssembler.kt @@ -46,7 +46,7 @@ class SearchWatcherSubAssembler( val cache: LocalCache, client: NostrClient, allKeys: () -> Set, -) : PerUniqueIdEoseManager(client, allKeys) { +) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: SearchQueryState, since: SincePerRelayMap?, @@ -95,5 +95,5 @@ class SearchWatcherSubAssembler( return directFilters + searchFilters } - override fun id(key: SearchQueryState) = key.searchQuery.hashCode().toString() + override fun id(key: SearchQueryState) = key.searchQuery.hashCode() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index 037c427e3b..1e24dc69e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.relays import androidx.collection.LruCache +import com.vitorpamplona.amethyst.model.LocalCache.users import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.ammolite.relays.filters.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -57,11 +58,15 @@ class EOSERelayList { class EOSEFollowList( cacheSize: Int = 200, +) : EOSEByKey(cacheSize) + +open class EOSEByKey( + cacheSize: Int = 200, ) { - var followList: LruCache = LruCache(cacheSize) + var followList: LruCache = LruCache(cacheSize) fun addOrUpdate( - listCode: String, + listCode: U, relayUrl: NormalizedRelayUrl, time: Long, ) { @@ -75,10 +80,10 @@ class EOSEFollowList( } } - fun since(listCode: String) = followList[listCode]?.relayList + fun since(listCode: U) = followList[listCode]?.relayList fun newEose( - listCode: String, + listCode: U, relayUrl: NormalizedRelayUrl, time: Long, ) = addOrUpdate(listCode, relayUrl, time) @@ -86,18 +91,22 @@ class EOSEFollowList( class EOSEAccount( cacheSize: Int = 20, +) : EOSEAccountKey(cacheSize) + +open class EOSEAccountKey( + cacheSize: Int = 20, ) { - var users: LruCache = LruCache(cacheSize) + var users: LruCache> = LruCache>(cacheSize) fun addOrUpdate( user: User, - listCode: String, + listCode: U, relayUrl: NormalizedRelayUrl, time: Long, ) { val followList = users[user] if (followList == null) { - val newList = EOSEFollowList() + val newList = EOSEByKey() users.put(user, newList) newList.addOrUpdate(listCode, relayUrl, time) } else { @@ -111,12 +120,12 @@ class EOSEAccount( fun since( key: User, - listCode: String, + listCode: U, ) = users[key]?.followList?.get(listCode)?.relayList fun newEose( user: User, - listCode: String, + listCode: U, relayUrl: NormalizedRelayUrl, time: Long, ) = addOrUpdate(user, listCode, relayUrl, time) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 5ec963e7b6..74affd46fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -40,11 +40,12 @@ import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.prepareSharedViewModel import com.vitorpamplona.amethyst.ui.theme.AmethystTheme -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed @@ -52,12 +53,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import kotlinx.coroutines.CancellationException import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -165,30 +161,33 @@ fun uriToRoute( is NProfile -> Route.Profile(nip19.hex) is NNote -> Route.Note(nip19.hex) is NEvent -> { - if (nip19.kind == PrivateDmEvent.KIND) { - nip19.author?.let { Route.RoomByAuthor(it) } - } else if ( - nip19.kind == ChannelMessageEvent.KIND || - nip19.kind == ChannelCreateEvent.KIND || - nip19.kind == ChannelMetadataEvent.KIND - ) { - Route.Channel(nip19.hex) - } else { - Route.EventRedirect(nip19.hex) - } + routeFor( + note = LocalCache.getOrCreateNote(nip19.hex), + loggedIn = account.userProfile(), + ) ?: Route.EventRedirect(nip19.hex) } is NAddress -> { - if (nip19.kind == CommunityDefinitionEvent.KIND) { - Route.Community(nip19.kind, nip19.author, nip19.dTag) - } else if (nip19.kind == LiveActivitiesEvent.KIND) { - Route.Channel(nip19.aTag()) - } else { - Route.EventRedirect(nip19.aTag()) - } + routeFor( + note = LocalCache.getOrCreateAddressableNote(nip19.address()), + loggedIn = account.userProfile(), + ) ?: Route.EventRedirect(nip19.aTag()) } - is NEmbed -> Route.EventRedirect(nip19.event.id) + is NEmbed -> { + val noteEvent = nip19.event + if (noteEvent is AddressableEvent) { + routeFor( + note = LocalCache.getOrCreateAddressableNote(noteEvent.address()), + loggedIn = account.userProfile(), + ) ?: Route.EventRedirect(noteEvent.addressTag()) + } else { + routeFor( + note = LocalCache.getOrCreateNote(nip19.event.id), + loggedIn = account.userProfile(), + ) ?: Route.EventRedirect(nip19.event.id) + } + } else -> null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index 4af0330a5f..e9812406bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -42,7 +42,6 @@ class NewMessageTagger( var message: String, var pTags: List? = null, var eTags: List? = null, - var channelHex: String? = null, var dao: Dao, ) { val directMentions = mutableSetOf() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index ce79850d6b..8c4a7b8458 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -30,7 +30,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -57,30 +56,27 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.routeFor -import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.njumpLink import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip19Bech32.toNIP19 -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap @@ -95,19 +91,13 @@ fun ClickableRoute( when (val entity = nip19.entity) { is NPub -> DisplayUser(entity.hex, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) is NProfile -> DisplayUser(entity.hex, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) - is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> DisplayEvent(entity.hex, null, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) - is NEvent -> DisplayEvent(entity.hex, entity.kind, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) + is NNote -> DisplayEvent(entity.hex, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) + is NEvent -> DisplayEvent(entity.hex, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) is NEmbed -> LoadAndDisplayEvent(entity.event, nip19.additionalChars, accountViewModel, nav) is NAddress -> DisplayAddress(entity, nip19.nip19raw, nip19.additionalChars, accountViewModel, nav) - is NRelay -> { - Text(word) - } - is NSec -> { - Text(word) - } - else -> { - Text(word) - } + is NRelay -> Text(word) + is NSec -> Text(word) + else -> Text(word) } } @@ -138,7 +128,7 @@ private fun LoadAndDisplayEvent( ) { LoadOrCreateNote(event, accountViewModel) { if (it != null) { - DisplayNoteLink(it, event.id, event.kind, additionalChars, accountViewModel, nav) + DisplayNoteLink(it, event.id, additionalChars, accountViewModel, nav) } else { val externalLink = event.toNIP19() val uri = LocalUriHandler.current @@ -158,7 +148,6 @@ private fun LoadAndDisplayEvent( @Composable fun DisplayEvent( hex: HexKey, - kind: Int?, nip19: String, additionalChars: String?, accountViewModel: AccountViewModel, @@ -166,7 +155,7 @@ fun DisplayEvent( ) { LoadNote(hex, accountViewModel) { if (it != null) { - DisplayNoteLink(it, hex, kind, additionalChars, accountViewModel, nav) + DisplayNoteLink(it, hex, additionalChars, accountViewModel, nav) } else { val externalLink = njumpLink(nip19) val uri = LocalUriHandler.current @@ -187,55 +176,21 @@ fun DisplayEvent( private fun DisplayNoteLink( it: Note, hex: HexKey, - kind: Int?, addedCharts: String?, accountViewModel: AccountViewModel, nav: INav, ) { val noteState by observeNote(it, accountViewModel) - val note = remember(noteState) { noteState?.note } ?: return + val noteIdDisplayNote = remember(noteState) { "@${noteState.note.idDisplayNote()}" } - val channelHex = remember(noteState) { note.channelHex() } - val noteIdDisplayNote = remember(noteState) { "@${note.idDisplayNote()}" } + val route = routeFor(it, accountViewModel.userProfile()) ?: Route.EventRedirect(hex) - if (note.event is ChannelCreateEvent || kind == ChannelCreateEvent.KIND) { - CreateClickableText( - clickablePart = noteIdDisplayNote, - suffix = addedCharts, - route = remember(noteState) { Route.Channel(hex) }, - nav = nav, - ) - } else if (note.event is PrivateDmEvent || kind == PrivateDmEvent.KIND) { - CreateClickableText( - clickablePart = noteIdDisplayNote, - suffix = addedCharts, - route = - remember(noteState) { (note.author?.pubkeyHex ?: hex).let { Route.RoomByAuthor(it) } }, - nav = nav, - ) - } else if (channelHex != null) { - LoadChannel(baseChannelHex = channelHex, accountViewModel) { baseChannel -> - val channelState by observeChannel(baseChannel, accountViewModel) - val channelDisplayName by - remember(channelState) { - derivedStateOf { channelState?.channel?.toBestDisplayName() ?: noteIdDisplayNote } - } - - CreateClickableText( - clickablePart = channelDisplayName, - suffix = addedCharts, - route = remember(noteState) { Route.Channel(baseChannel.idHex) }, - nav = nav, - ) - } - } else { - CreateClickableText( - clickablePart = noteIdDisplayNote, - suffix = addedCharts, - route = remember(noteState) { Route.EventRedirect(hex) }, - nav = nav, - ) - } + CreateClickableText( + clickablePart = noteIdDisplayNote, + suffix = addedCharts, + route = route, + nav = nav, + ) } @Composable @@ -258,7 +213,7 @@ private fun DisplayAddress( val noteState by observeNote(it, accountViewModel) val route = remember(noteState) { Route.Note(nip19.aTag()) } - val displayName = remember(noteState) { "@${noteState?.note?.idDisplayNote()}" } + val displayName = remember(noteState) { "@${noteState.note.idDisplayNote()}" } CreateClickableText( clickablePart = displayName, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 7a9a8f6a31..5670c32667 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -94,7 +94,7 @@ import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview -import com.vitorpamplona.amethyst.ui.note.toShortenHex +import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel @@ -727,7 +727,7 @@ fun TagLink( ) { LoadNote(baseNoteHex = word.hex, accountViewModel) { if (it == null) { - Text(text = remember { word.segmentText.toShortenHex() }) + Text(text = remember { word.segmentText.toShortDisplay() }) } else { Row { DisplayNoteFromTag( @@ -766,7 +766,7 @@ private fun DisplayNoteFromTag( ) } else { ClickableTextPrimary( - text = "@${baseNote.idNote().toShortenHex()}", + text = "@${baseNote.idNote().toShortDisplay()}", onClick = { routeFor(baseNote, accountViewModel.userProfile())?.let { nav.nav(it) } }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt index abd5be836b..5f0df79fe4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -35,7 +35,7 @@ abstract class FeedFilter { open fun limit() = 1000 /** Returns a string that serves as the key to invalidate the list if it changes. */ - abstract fun feedKey(): String + abstract fun feedKey(): Any open fun showHiddenKey(): Boolean = false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt index 0ea301bce8..cb6998bd5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt @@ -24,7 +24,7 @@ import android.util.Log import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.AdditiveComplexFeedFilter @@ -42,7 +42,7 @@ import kotlin.collections.distinctBy @Stable class ChannelFeedContentState( - val localFilter: AdditiveComplexFeedFilter, + val localFilter: AdditiveComplexFeedFilter, val viewModelScope: CoroutineScope, ) : InvalidatableContent { private val _feedContent = MutableStateFlow(ChannelFeedState.Loading) @@ -53,7 +53,7 @@ class ChannelFeedContentState( val scrollToTop = _scrollToTop.asStateFlow() var scrolltoTopPending = false - private var lastFeedKey: String? = null + private var lastFeedKey: Any? = null override val isRefreshing: MutableState = mutableStateOf(false) @@ -78,7 +78,7 @@ class ChannelFeedContentState( isRefreshing.value = true try { lastFeedKey = localFilter.feedKey() - val notes = localFilter.loadTop().distinctBy { it.idHex }.toImmutableList() + val notes = localFilter.loadTop().distinctBy { it }.toImmutableList() val oldNotesState = _feedContent.value if (oldNotesState is ChannelFeedState.Loaded) { @@ -93,15 +93,15 @@ class ChannelFeedContentState( } } - private fun updateFeed(notes: ImmutableList) { + private fun updateFeed(notes: ImmutableList) { val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.tryEmit(ChannelFeedState.Empty) } else if (currentState is ChannelFeedState.Loaded) { - currentState.feed.tryEmit(LoadedFeedState(notes, localFilter.showHiddenKey())) + currentState.feed.tryEmit(LoadedFeedState(notes, localFilter.showHiddenKey())) } else { _feedContent.tryEmit( - ChannelFeedState.Loaded(MutableStateFlow(LoadedFeedState(notes, localFilter.showHiddenKey()))), + ChannelFeedState.Loaded(MutableStateFlow(LoadedFeedState(notes, localFilter.showHiddenKey()))), ) } } @@ -123,7 +123,7 @@ class ChannelFeedContentState( val newList = localFilter .updateListWith(emptyList(), newItems) - .distinctBy { it.idHex } + .distinctBy { it } .toImmutableList() if (newList.isNotEmpty()) { updateFeed(newList) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt index 123a3e4d6d..8bf9e8cd35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.EphemeralChatChannel import kotlinx.coroutines.flow.MutableStateFlow @Stable @@ -29,7 +29,7 @@ sealed class ChannelFeedState { object Loading : ChannelFeedState() class Loaded( - val feed: MutableStateFlow>, + val feed: MutableStateFlow>, ) : ChannelFeedState() object Empty : ChannelFeedState() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt index ffc7656e3e..4602d59749 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt @@ -53,7 +53,7 @@ class FeedContentState( val scrollToTop = _scrollToTop.asStateFlow() var scrolltoTopPending = false - private var lastFeedKey: String? = null + private var lastFeedKey: Any? = null override val isRefreshing: MutableState = mutableStateOf(false) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt index ca3e7da9c8..631b36d75a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -64,7 +64,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.note.toShortenHex +import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog @@ -242,7 +242,7 @@ private fun AccountName( } Text( - text = remember(user) { acc.npub.toShortenHex() }, + text = remember(user) { acc.npub.toShortDisplay() }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index da00bc896a..863975fecd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -59,10 +59,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScree import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.NewGroupDMScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.EphemeralChatScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.EphemeralChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.CommunityScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen @@ -138,9 +139,12 @@ fun AppNavigation( composableFromEndArgs { GeoHashScreen(it, accountViewModel, nav) } composableFromEndArgs { RelayInformationScreen(it.url, accountViewModel, nav) } composableFromEndArgs { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) } + composableFromEndArgs { ChatroomScreen(it.id.toString(), it.message, it.replyId, it.draftId, accountViewModel, nav) } composableFromEndArgs { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) } - composableFromEndArgs { ChannelScreen(it.id, accountViewModel, nav) } + + composableFromEndArgs { PublicChatChannelScreen(it.id, accountViewModel, nav) } + composableFromEndArgs { LiveActivityChannelScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) } composableFromEndArgs { RelayUrlNormalizer.normalizeOrNull(it.relayUrl)?.let { relay -> EphemeralChatScreen(RoomId(it.id, relay), accountViewModel, nav) @@ -388,7 +392,7 @@ private fun isSameRoute( if (newRoute is Route.EventRedirect) { return when (currentRoute) { is Route.Note -> newRoute.id == currentRoute.id - is Route.Channel -> newRoute.id == currentRoute.id + is Route.PublicChatChannel -> newRoute.id == currentRoute.id else -> false } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt index c48c8d8341..831e692c8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt @@ -20,9 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.navigation -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId @@ -39,6 +41,8 @@ import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip73ExternalIds.location.isGeohashedScoped import com.vitorpamplona.quartz.nip73ExternalIds.topics.isHashtagScoped @@ -48,7 +52,7 @@ fun routeFor( note: Note, loggedIn: User, ): Route? { - val noteEvent = note.event ?: return Route.Note(note.idHex) + val noteEvent = note.event ?: return Route.EventRedirect(note.idHex) return routeFor(noteEvent, loggedIn) } @@ -62,15 +66,15 @@ fun routeFor( if (innerEvent is IsInPublicChatChannel) { innerEvent.channelId()?.let { - return Route.Channel(it) + return Route.PublicChatChannel(it) } } else if (innerEvent is LiveActivitiesEvent) { - innerEvent.aTag().toTag().let { - return Route.Channel(it) + innerEvent.address().let { + return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag) } } else if (innerEvent is LiveActivitiesChatMessageEvent) { - innerEvent.activity()?.toTag()?.let { - return Route.Channel(it) + innerEvent.activityAddress()?.let { + return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag) } } else if (innerEvent is ChatroomKeyable) { val room = innerEvent.chatroomKey(loggedIn.pubkeyHex) @@ -85,17 +89,17 @@ fun routeFor( return Route.ContentDiscovery(noteEvent.id) } else if (noteEvent is IsInPublicChatChannel) { noteEvent.channelId()?.let { - return Route.Channel(it) + return Route.PublicChatChannel(it) } } else if (noteEvent is ChannelCreateEvent) { - return Route.Channel(noteEvent.id) + return Route.PublicChatChannel(noteEvent.id) } else if (noteEvent is LiveActivitiesEvent) { - noteEvent.aTag().toTag().let { - return Route.Channel(it) + noteEvent.address().let { + return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag) } } else if (noteEvent is LiveActivitiesChatMessageEvent) { - noteEvent.activity()?.toTag()?.let { - return Route.Channel(it) + noteEvent.activityAddress()?.let { + return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag) } } else if (noteEvent is ChatroomKeyable) { val room = noteEvent.chatroomKey(loggedIn.pubkeyHex) @@ -103,6 +107,14 @@ fun routeFor( return Route.Room(room.hashCode()) } else if (noteEvent is CommunityDefinitionEvent) { return Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag()) + } else if (noteEvent is GiftWrapEvent) { + noteEvent.innerEventId?.let { + return routeFor(LocalCache.getOrCreateNote(it), loggedIn) + } + } else if (noteEvent is SealedRumorEvent) { + noteEvent.innerEventId?.let { + return routeFor(LocalCache.getOrCreateNote(it), loggedIn) + } } else if (noteEvent is AddressableEvent) { return Route.Note(noteEvent.aTag().toTag()) } else { @@ -169,12 +181,11 @@ fun routeToMessage( accountViewModel: AccountViewModel, ): Route = routeToMessage(user.pubkeyHex, draftMessage, replyId, draftId, accountViewModel) -fun routeFor(note: Channel): Route = - if (note is EphemeralChatChannel) { - Route.EphemeralChat(note.roomId.id, note.roomId.relayUrl.url) - } else { - Route.Channel(note.idHex) - } +fun routeFor(note: EphemeralChatChannel): Route = Route.EphemeralChat(note.roomId.id, note.roomId.relayUrl.url) + +fun routeFor(note: PublicChatChannel): Route = Route.PublicChatChannel(note.idHex) + +fun routeFor(note: LiveActivitiesChannel): Route = Route.LiveActivityChannel(note.address.kind, note.address.pubKeyHex, note.address.dTag) fun routeFor(roomId: RoomId): Route = Route.EphemeralChat(roomId.id, roomId.relayUrl.url) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index cd27911eff..bbabd85617 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -85,7 +85,17 @@ sealed class Route { val dTag: String, ) : Route() - @Serializable data class Channel( + @Serializable data class PublicChatChannel( + val id: String, + ) : Route() + + @Serializable data class LiveActivityChannel( + val kind: Int, + val pubKeyHex: HexKey, + val dTag: String, + ) : Route() + + @Serializable data class EphemeralChatChannel( val id: String, ) : Route() @@ -204,7 +214,8 @@ fun getRouteWithArguments(navController: NavHostController): Route? { dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() - dest.hasRoute() -> entry.toRoute() + dest.hasRoute() -> entry.toRoute() + dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 9fa960be45..9a41ff5b1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -31,19 +31,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext @@ -161,36 +159,28 @@ fun LoadOts( } @Composable -fun LoadChannel( - baseChannelHex: String, +fun LoadPublicChatChannel( + id: String, accountViewModel: AccountViewModel, - content: @Composable (Channel) -> Unit, + content: @Composable (PublicChatChannel) -> Unit, ) { - var channel by - remember(baseChannelHex) { - mutableStateOf(accountViewModel.getChannelIfExists(baseChannelHex)) + val channel = + produceStateIfNotNull(accountViewModel.getPublicChatChannelIfExists(id), id) { + value = accountViewModel.checkGetOrCreatePublicChatChannel(id) } - if (channel == null) { - LaunchedEffect(key1 = baseChannelHex) { - accountViewModel.checkGetOrCreateChannel(baseChannelHex) { newChannel -> - launch(Dispatchers.Main) { channel = newChannel } - } - } - } - - channel?.let { content(it) } + channel.value?.let { content(it) } } @Composable -fun LoadChannel( - id: RoomId, +fun LoadLiveActivityChannel( + id: Address, accountViewModel: AccountViewModel, - content: @Composable (EphemeralChatChannel) -> Unit, + content: @Composable (LiveActivitiesChannel) -> Unit, ) { val channel = - produceStateIfNotNull(accountViewModel.getChannelIfExists(id) as? EphemeralChatChannel, id) { - value = accountViewModel.checkGetOrCreateChannel(id) as? EphemeralChatChannel + produceStateIfNotNull(accountViewModel.getLiveActivityChannelIfExists(id), id) { + value = accountViewModel.checkGetOrCreateLiveActivityChannel(id) } channel.value?.let { content(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 3c7c1a249d..0a8dde8252 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -52,9 +52,9 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent @@ -125,7 +125,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment import com.vitorpamplona.amethyst.ui.note.types.RenderWikiContent import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.RenderChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.RenderPublicChatChannelHeader import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.Font12SP @@ -276,9 +276,8 @@ fun AcceptableNote( is ChannelCreateEvent, is ChannelMetadataEvent, -> - RenderChannelHeader( + RenderPublicChatChannelHeader( channelNote = baseNote, - showVideo = !makeItShort, sendToChannel = true, accountViewModel = accountViewModel, nav = nav, @@ -316,9 +315,8 @@ fun AcceptableNote( is ChannelCreateEvent, is ChannelMetadataEvent, -> - RenderChannelHeader( + RenderPublicChatChannelHeader( channelNote = baseNote, - showVideo = !makeItShort, sendToChannel = true, accountViewModel = accountViewModel, nav = nav, @@ -1186,7 +1184,7 @@ private fun RenderAuthorImages( if (baseNote.event is ChannelMessageEvent) { val baseChannelHex = remember(baseNote) { baseNote.channelHex() } if (baseChannelHex != null) { - LoadChannel(baseChannelHex, accountViewModel) { channel -> + LoadPublicChatChannel(baseChannelHex, accountViewModel) { channel -> ChannelNotePicture( channel, accountViewModel, @@ -1198,7 +1196,7 @@ private fun RenderAuthorImages( @Composable private fun ChannelNotePicture( - baseChannel: Channel, + baseChannel: PublicChatChannel, accountViewModel: AccountViewModel, ) { val model by observeChannelPicture(baseChannel, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt index d9e29088bf..4ba6a2f3f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt @@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.ui.note import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey -fun ByteArray.toShortenHex(): String = toHexKey().toShortenHex() +fun ByteArray.toHexShortDisplay(): String = toHexKey().toShortDisplay() -fun String.toShortenHex(): String { +fun String.toShortDisplay(): String { if (length <= 16) return this return replaceRange(8, length - 8, ":") } -fun HexKey.toDisplayHexKey(): String = this.toShortenHex() +fun HexKey.toDisplayHexKey(): String = this.toShortDisplay() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index a28cb0839c..9750c6d93c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -656,7 +656,7 @@ open class CommentPostViewModel : override fun updateZapFromText() { viewModelScope.launch(Dispatchers.Default) { val tagger = - NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!) + NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!) tagger.run() tagger.pTags?.forEach { taggedUser -> if (!forwardZapTo.value.items.any { it.key == taggedUser }) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt index 75aa3569bb..7b2923217b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelHeader import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @@ -60,9 +60,8 @@ fun RenderChannelMessage( } showChannelInfo?.let { - ChannelHeader( + PublicChatChannelHeader( channelHex = it, - showVideo = false, sendToChannel = true, modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), accountViewModel = accountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index 80063464de..f6a5f2f93b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -139,7 +139,7 @@ fun RenderLiveActivityEventInner( val subject = remember(eventUpdates) { noteEvent.title() } val content = remember(eventUpdates) { noteEvent.summary() } val participants = remember(eventUpdates) { noteEvent.participants() } - val status = remember(eventUpdates) { noteEvent.status() } + val status = remember(eventUpdates) { noteEvent.statusEnum() } val starts = remember(eventUpdates) { noteEvent.starts() } Row( @@ -163,19 +163,22 @@ fun RenderLiveActivityEventInner( CrossfadeIfEnabled(targetState = status, label = "RenderLiveActivityEventInner", accountViewModel = accountViewModel) { when (it) { - StatusTag.STATUS.LIVE.code -> { + StatusTag.STATUS.LIVE -> { media?.let { CrossfadeCheckIfVideoIsOnline(it, accountViewModel) { LiveFlag() } } } - StatusTag.STATUS.PLANNED.code -> { + StatusTag.STATUS.PLANNED -> { ScheduledFlag(starts) } + + StatusTag.STATUS.ENDED -> {} + null -> {} } } } media?.let { media -> - if (status == StatusTag.STATUS.LIVE.code) { + if (status == StatusTag.STATUS.LIVE) { CheckIfVideoIsOnline(media, accountViewModel) { isOnline -> if (isOnline) { Row( @@ -210,7 +213,7 @@ fun RenderLiveActivityEventInner( } } } else { - if (status == StatusTag.STATUS.ENDED.code || (status == StatusTag.STATUS.PLANNED.code && (starts ?: 0) < TimeUtils.eightHoursAgo())) { + if (status == StatusTag.STATUS.ENDED || (status == StatusTag.STATUS.PLANNED && (starts ?: 0) < TimeUtils.eightHoursAgo())) { Box( contentAlignment = Alignment.Center, modifier = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt index d66ac09bcc..100f091e8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt @@ -32,8 +32,9 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivitiesChannelHeader import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -53,21 +54,24 @@ fun RenderLiveActivityChatMessage( val showChannelInfo = remember(noteEvent) { if (noteEvent is LiveActivitiesChatMessageEvent) { - noteEvent.activity()?.toTag() + noteEvent.activityAddress() } else { null } } showChannelInfo?.let { - ChannelHeader( - channelHex = it, - showVideo = false, - sendToChannel = true, - modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), - accountViewModel = accountViewModel, - nav = nav, - ) + LoadLiveActivityChannel(it, accountViewModel) { + LiveActivitiesChannelHeader( + baseChannel = it, + showVideo = false, + showFlag = true, + sendToChannel = true, + modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), + accountViewModel = accountViewModel, + nav = nav, + ) + } Spacer(modifier = StdVertSpacer) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index d42ff041ae..18bbf3733b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.PublicChatChannel @@ -1116,20 +1117,24 @@ class AccountViewModel( ) } - private suspend fun checkGetOrCreateChannel(key: HexKey): Channel? = LocalCache.checkGetOrCreateChannel(key) + suspend fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel? = LocalCache.getOrCreatePublicChatChannel(key) - suspend fun checkGetOrCreateChannel(key: RoomId): Channel? = LocalCache.getOrCreateEphemeralChannel(key) + suspend fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel? = LocalCache.getOrCreateLiveChannel(key) + + suspend fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel? = LocalCache.getOrCreateEphemeralChannel(key) fun checkGetOrCreateChannel( key: HexKey, onResult: (Channel?) -> Unit, ) { - viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateChannel(key)) } + viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreatePublicChatChannel(key)) } } - fun getChannelIfExists(hex: HexKey): Channel? = LocalCache.getChannelIfExists(hex) + fun getPublicChatChannelIfExists(hex: HexKey) = LocalCache.getPublicChatChannelIfExists(hex) - fun getChannelIfExists(key: RoomId): Channel? = LocalCache.getChannelIfExists(key.toKey()) + fun getEphemeralChatChannelIfExists(key: RoomId) = LocalCache.getEphemeralChatChannelIfExists(key) + + fun getLiveActivityChannelIfExists(key: Address) = LocalCache.getLiveActivityChannelIfExists(key) fun loadParticipants( participants: List, @@ -1226,7 +1231,7 @@ class AccountViewModel( route?.let { if (route is Route.Room) { account.markAsRead("Room/${route.id}", date) - } else if (route is Route.Channel) { + } else if (route is Route.PublicChatChannel) { account.markAsRead("Channel/${route.id}", date) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index e63a68a008..4d917f9800 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.Alignment.Companion.CenterStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.INav @@ -60,7 +59,6 @@ import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW -import com.vitorpamplona.amethyst.ui.note.externalLinkForNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChangeChannelMetadataNote @@ -99,29 +97,25 @@ fun ChatroomMessageCompose( onWantsToReply: (Note) -> Unit, onWantsToEditDraft: (Note) -> Unit, ) { - logTime( - debugMessage = { "ChatroomMessageCompose " + externalLinkForNote(baseNote) }, - ) { - WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) { - WatchBlockAndReport( - note = baseNote, - showHiddenWarning = false, - modifier = Modifier.fillMaxWidth(), - accountViewModel = accountViewModel, - nav = nav, - ) { canPreview -> - NormalChatNote( - baseNote, - routeForLastRead, - innerQuote, - canPreview, - parentBackgroundColor, - accountViewModel, - nav, - onWantsToReply, - onWantsToEditDraft, - ) - } + WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel) { + WatchBlockAndReport( + note = baseNote, + showHiddenWarning = false, + modifier = Modifier.fillMaxWidth(), + accountViewModel = accountViewModel, + nav = nav, + ) { canPreview -> + NormalChatNote( + baseNote, + routeForLastRead, + innerQuote, + canPreview, + parentBackgroundColor, + accountViewModel, + nav, + onWantsToReply, + onWantsToEditDraft, + ) } } } @@ -176,7 +170,7 @@ fun NormalChatNote( parentBackgroundColor = parentBackgroundColor, onClick = { if (note.event is ChannelCreateEvent) { - nav.nav(Route.Channel(note.idHex)) + nav.nav(Route.PublicChatChannel(note.idHex)) true } else { false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt index 04469d6111..bfa91e4d8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/ChatroomFilterSubAssembler.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient class ChatroomFilterSubAssembler( client: NostrClient, allKeys: () -> Set, -) : PerUserAndFollowListEoseManager(client, allKeys) { +) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: ChatroomQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 196c7dc455..c1f73520ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -578,7 +578,7 @@ class ChatNewMessageViewModel : fun updateRoomFromUsersInput() { viewModelScope.launch(Dispatchers.Default) { delay(300) - val toUsersTagger = NewMessageTagger(toUsers.text, null, null, null, accountViewModel!!) + val toUsersTagger = NewMessageTagger(toUsers.text, null, null, accountViewModel!!) toUsersTagger.run() val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex } @@ -701,7 +701,7 @@ class ChatNewMessageViewModel : override fun updateZapFromText() { viewModelScope.launch(Dispatchers.Default) { - val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!) + val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!) tagger.run() tagger.pTags?.forEach { taggedUser -> if (!forwardZapTo.value.items.any { it.key == taggedUser }) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt index 56fdd2f5f5..b2119942e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt @@ -30,7 +30,7 @@ class ChannelFeedFilter( val channel: Channel, val account: Account, ) : AdditiveFeedFilter() { - override fun feedKey(): String = channel.idHex + override fun feedKey() = channel // returns the last Note of each user. override fun feed(): List = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt index d9f72f4dc7..398515fc9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.PublicChatChannel @@ -32,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ChannelFromUserFilterSubAssembler( client: NostrClient, allKeys: () -> Set, -) : PerUserAndFollowListEoseManager(client, allKeys) { +) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: ChannelQueryState, since: SincePerRelayMap?, @@ -48,5 +49,5 @@ class ChannelFromUserFilterSubAssembler( override fun user(key: ChannelQueryState) = key.account.userProfile() - override fun list(key: ChannelQueryState) = key.channel.idHex + override fun list(key: ChannelQueryState) = key.channel } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt index 31455be31a..4f6c3bf5df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies +import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.PublicChatChannel @@ -32,7 +33,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ChannelPublicFilterSubAssembler( client: NostrClient, allKeys: () -> Set, -) : PerUniqueIdEoseManager(client, allKeys) { +) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: ChannelQueryState, since: SincePerRelayMap?, @@ -44,5 +45,5 @@ class ChannelPublicFilterSubAssembler( else -> null } - override fun id(key: ChannelQueryState) = key.channel.idHex + override fun id(key: ChannelQueryState) = key.channel } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt index f11426e9e6..fb23067107 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt @@ -36,7 +36,7 @@ fun filterMessagesToLiveActivities( filter = Filter( kinds = listOf(LiveActivitiesChatMessageEvent.KIND), - tags = mapOf("a" to listOfNotNull(channel.idHex)), + tags = mapOf("a" to listOfNotNull(channel.address.toValue())), limit = 200, since = since?.get(it)?.time, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt index 509b113340..c020a9a6ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt @@ -38,7 +38,7 @@ fun filterMyMessagesToLiveActivities( filter = Filter( kinds = listOf(LiveActivitiesChatMessageEvent.KIND), - tags = mapOf("a" to listOfNotNull(channel.idHex)), + tags = mapOf("a" to listOfNotNull(channel.address.toValue())), authors = listOf(pubKey), limit = 50, since = since?.get(it)?.time, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt new file mode 100644 index 0000000000..4df7c2ffe0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId +import kotlinx.coroutines.launch + +@Composable +fun EphemeralChatChannelView( + channelId: RoomId?, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (channelId == null) return + + LoadEphemeralChatChannel(channelId, accountViewModel) { ephem -> + PrepareChannelViewModels( + baseChannel = ephem, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} + +@Composable +private fun PrepareChannelViewModels( + baseChannel: EphemeralChatChannel, + accountViewModel: AccountViewModel, + nav: INav, +) { + val feedViewModel: ChannelFeedViewModel = + viewModel( + key = baseChannel.roomId.toKey() + "ChannelFeedViewModel", + factory = + ChannelFeedViewModel.Factory( + baseChannel, + accountViewModel.account, + ), + ) + + val channelScreenModel: ChannelNewMessageViewModel = viewModel() + channelScreenModel.init(accountViewModel) + channelScreenModel.load(baseChannel) + + ChannelView( + channel = baseChannel, + feedViewModel = feedViewModel, + newPostModel = channelScreenModel, + accountViewModel = accountViewModel, + nav = nav, + ) +} + +@Composable +private fun ChannelView( + channel: EphemeralChatChannel, + feedViewModel: ChannelFeedViewModel, + newPostModel: ChannelNewMessageViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + WatchLifecycleAndUpdateModel(feedViewModel) + ChannelFilterAssemblerSubscription(channel, accountViewModel.dataSources().channel, accountViewModel) + + Column(Modifier.fillMaxHeight()) { + Column( + modifier = + remember { + Modifier + .fillMaxHeight() + .padding(vertical = 0.dp) + .weight(1f, true) + }, + ) { + RefreshingChatroomFeedView( + viewModel = feedViewModel, + accountViewModel = accountViewModel, + nav = nav, + routeForLastRead = "Channel/${channel.roomId.toKey()}", + avoidDraft = newPostModel.draftTag, + onWantsToReply = newPostModel::reply, + onWantsToEditDraft = newPostModel::editFromDraft, + ) + } + + Spacer(modifier = DoubleVertSpacer) + + val scope = rememberCoroutineScope() + + // LAST ROW + EditFieldRow( + newPostModel, + accountViewModel, + onSendNewMessage = { + scope.launch { + feedViewModel.sendToTop() + } + }, + nav, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt similarity index 60% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelScreen.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt index 4df883025c..6287ebb2a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt @@ -18,51 +18,18 @@ * 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.chats.publicChannels +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.EphemeralChatTopBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatTopBar -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivityTopBar import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId -@Composable -fun ChannelScreen( - channelId: String?, - accountViewModel: AccountViewModel, - nav: INav, -) { - if (channelId == null) return - - DisappearingScaffold( - isInvertedLayout = true, - topBar = { - LoadChannel(channelId, accountViewModel) { - when (it) { - is EphemeralChatChannel -> EphemeralChatTopBar(it, accountViewModel, nav) - is PublicChatChannel -> PublicChatTopBar(it, accountViewModel, nav) - is LiveActivitiesChannel -> LiveActivityTopBar(it, accountViewModel, nav) - } - } - }, - accountViewModel = accountViewModel, - ) { - Column(Modifier.padding(it)) { - ChannelView(channelId, accountViewModel, nav) - } - } -} - @Composable fun EphemeralChatScreen( channelId: RoomId, @@ -72,14 +39,14 @@ fun EphemeralChatScreen( DisappearingScaffold( isInvertedLayout = true, topBar = { - LoadChannel(channelId, accountViewModel) { + LoadEphemeralChatChannel(channelId, accountViewModel) { EphemeralChatTopBar(it, accountViewModel, nav) } }, accountViewModel = accountViewModel, ) { Column(Modifier.padding(it)) { - ChannelView(channelId, accountViewModel, nav) + EphemeralChatChannelView(channelId, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt new file mode 100644 index 0000000000..fd87e59576 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.ui.note.produceStateIfNotNull +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId + +@Composable +fun LoadEphemeralChatChannel( + id: RoomId, + accountViewModel: AccountViewModel, + content: @Composable (EphemeralChatChannel) -> Unit, +) { + val channel = + produceStateIfNotNull(accountViewModel.getEphemeralChatChannelIfExists(id), id) { + value = accountViewModel.checkGetOrCreateEphemeralChatChannel(id) + } + + channel.value?.let { content(it) } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt index 795636635e..17f80f545a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt @@ -123,7 +123,7 @@ private fun DrawRelayIcon( val relayInfo by loadRelayInfo(channel.roomId.relayUrl, accountViewModel) RobohashFallbackAsyncImage( - robot = channel.idHex, + robot = channel.roomId.toKey(), model = relayInfo?.icon, contentDescription = stringRes(R.string.profile_image), contentScale = ContentScale.Crop, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt similarity index 83% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelView.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index 5183e84398..bf720d9e50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -30,48 +30,28 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.LoadChannel +import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ShowVideoStreaming import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import kotlinx.coroutines.launch @Composable -fun ChannelView( +fun PublicChatChannelView( channelId: String?, accountViewModel: AccountViewModel, nav: INav, ) { if (channelId == null) return - LoadChannel(channelId, accountViewModel) { - PrepareChannelViewModels( - baseChannel = it, - accountViewModel = accountViewModel, - nav = nav, - ) - } -} - -@Composable -fun ChannelView( - channelId: RoomId?, - accountViewModel: AccountViewModel, - nav: INav, -) { - if (channelId == null) return - - LoadChannel(channelId, accountViewModel) { + LoadPublicChatChannel(channelId, accountViewModel) { PrepareChannelViewModels( baseChannel = it, accountViewModel = accountViewModel, @@ -82,7 +62,7 @@ fun ChannelView( @Composable fun PrepareChannelViewModels( - baseChannel: Channel, + baseChannel: PublicChatChannel, accountViewModel: AccountViewModel, nav: INav, ) { @@ -111,7 +91,7 @@ fun PrepareChannelViewModels( @Composable fun ChannelView( - channel: Channel, + channel: PublicChatChannel, feedViewModel: ChannelFeedViewModel, newPostModel: ChannelNewMessageViewModel, accountViewModel: AccountViewModel, @@ -130,9 +110,6 @@ fun ChannelView( .weight(1f, true) }, ) { - if (channel is LiveActivitiesChannel) { - ShowVideoStreaming(channel, accountViewModel) - } RefreshingChatroomFeedView( viewModel = feedViewModel, accountViewModel = accountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt similarity index 60% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelHeader.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt index 04844e8cc1..7ee8a6c243 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt @@ -18,38 +18,31 @@ * 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.chats.publicChannels +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.LoadChannel +import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.EphemeralChatChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatChannelHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivitiesChannelHeader import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.innerPostModifier @Composable -fun RenderChannelHeader( +fun RenderPublicChatChannelHeader( channelNote: Note, - showVideo: Boolean, sendToChannel: Boolean, accountViewModel: AccountViewModel, nav: INav, ) { channelNote.channelHex()?.let { - ChannelHeader( + PublicChatChannelHeader( channelHex = it, - showVideo = showVideo, sendToChannel = sendToChannel, modifier = MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp), accountViewModel = accountViewModel, @@ -59,44 +52,20 @@ fun RenderChannelHeader( } @Composable -fun ChannelHeader( +fun PublicChatChannelHeader( channelHex: String, - showVideo: Boolean, - showFlag: Boolean = true, sendToChannel: Boolean = false, modifier: Modifier = StdPadding, accountViewModel: AccountViewModel, nav: INav, ) { - LoadChannel(channelHex, accountViewModel) { - when (it) { - is LiveActivitiesChannel -> - LiveActivitiesChannelHeader( - it, - showVideo, - showFlag, - sendToChannel, - modifier, - accountViewModel, - nav, - ) - is PublicChatChannel -> - PublicChatChannelHeader( - it, - sendToChannel, - modifier, - accountViewModel, - nav, - ) - - is EphemeralChatChannel -> - EphemeralChatChannelHeader( - it, - sendToChannel, - modifier, - accountViewModel, - nav, - ) - } + LoadPublicChatChannel(channelHex, accountViewModel) { + PublicChatChannelHeader( + it, + sendToChannel, + modifier, + accountViewModel, + nav, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt new file mode 100644 index 0000000000..2e3f46e274 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatTopBar +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +@Composable +fun PublicChatChannelScreen( + channelId: HexKey?, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (channelId == null) return + + DisappearingScaffold( + isInvertedLayout = true, + topBar = { + LoadPublicChatChannel(channelId, accountViewModel) { + PublicChatTopBar(it, accountViewModel, nav) + } + }, + accountViewModel = accountViewModel, + ) { + Column(Modifier.padding(it)) { + PublicChatChannelView(channelId, accountViewModel, nav) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index b5a872aee5..db551defb1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -58,7 +58,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.ui.navigation.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.routeFor -import com.vitorpamplona.amethyst.ui.note.LoadChannel +import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -86,7 +86,7 @@ fun ChannelMetadataScreen( if (channelId == null) { ChannelMetadataScreen(null as PublicChatChannel?, accountViewModel, nav) } else { - LoadChannel(channelId, accountViewModel) { + LoadPublicChatChannel(channelId, accountViewModel) { if (it is PublicChatChannel) { ChannelMetadataScreen(it, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt new file mode 100644 index 0000000000..4b6471beb4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import kotlinx.coroutines.launch + +@Composable +fun LiveActivityChannelView( + channelId: Address?, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (channelId == null) return + + LoadLiveActivityChannel(channelId, accountViewModel) { + PrepareChannelViewModels( + baseChannel = it, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} + +@Composable +fun PrepareChannelViewModels( + baseChannel: LiveActivitiesChannel, + accountViewModel: AccountViewModel, + nav: INav, +) { + val feedViewModel: ChannelFeedViewModel = + viewModel( + key = baseChannel.address.toValue() + "ChannelFeedViewModel", + factory = + ChannelFeedViewModel.Factory( + baseChannel, + accountViewModel.account, + ), + ) + + val channelScreenModel: ChannelNewMessageViewModel = viewModel() + channelScreenModel.init(accountViewModel) + channelScreenModel.load(baseChannel) + + LiveActivityChannelView( + channel = baseChannel, + feedViewModel = feedViewModel, + newPostModel = channelScreenModel, + accountViewModel = accountViewModel, + nav = nav, + ) +} + +@Composable +fun LiveActivityChannelView( + channel: LiveActivitiesChannel, + feedViewModel: ChannelFeedViewModel, + newPostModel: ChannelNewMessageViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + WatchLifecycleAndUpdateModel(feedViewModel) + ChannelFilterAssemblerSubscription(channel, accountViewModel.dataSources().channel, accountViewModel) + + Column(Modifier.fillMaxHeight()) { + Column( + modifier = + remember { + Modifier + .fillMaxHeight() + .padding(vertical = 0.dp) + .weight(1f, true) + }, + ) { + ShowVideoStreaming(channel, accountViewModel) + RefreshingChatroomFeedView( + viewModel = feedViewModel, + accountViewModel = accountViewModel, + nav = nav, + routeForLastRead = "Channel/${channel.address.toValue()}", + avoidDraft = newPostModel.draftTag, + onWantsToReply = newPostModel::reply, + onWantsToEditDraft = newPostModel::editFromDraft, + ) + } + + Spacer(modifier = DoubleVertSpacer) + + val scope = rememberCoroutineScope() + + // LAST ROW + EditFieldRow( + newPostModel, + accountViewModel, + onSendNewMessage = { + scope.launch { + feedViewModel.sendToTop() + } + }, + nav, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt index a02f1db605..a6c5623817 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt @@ -64,9 +64,9 @@ fun LiveActivitiesChannelHeader( ) { ShortLiveActivityChannelHeader( baseChannel = baseChannel, + showFlag = showFlag, accountViewModel = accountViewModel, nav = nav, - showFlag = showFlag, ) if (expanded.value) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt new file mode 100644 index 0000000000..f686c5c946 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivityTopBar +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address + +@Composable +fun LiveActivityChannelScreen( + channelId: Address?, + accountViewModel: AccountViewModel, + nav: INav, +) { + if (channelId == null) return + + DisappearingScaffold( + isInvertedLayout = true, + topBar = { + LoadLiveActivityChannel(channelId, accountViewModel) { + LiveActivityTopBar(it, accountViewModel, nav) + } + }, + accountViewModel = accountViewModel, + ) { + Column(Modifier.padding(it)) { + LiveActivityChannelView(channelId, accountViewModel, nav) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveChannelActionOptions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveChannelActionOptions.kt deleted file mode 100644 index 865ee7284d..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveChannelActionOptions.kt +++ /dev/null @@ -1,77 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.ZapReaction -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.theme.RowColSpacing -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag - -@Composable -fun LiveChannelActionOptions( - channel: LiveActivitiesChannel, - showFlag: Boolean = true, - accountViewModel: AccountViewModel, - nav: INav, -) { - val isLive by remember(channel) { derivedStateOf { channel.info?.status() == StatusTag.STATUS.LIVE.code } } - - val note = remember(channel.idHex) { LocalCache.getNoteIfExists(channel.idHex) } - - note?.let { - if (showFlag && isLive) { - LiveFlag() - Spacer(modifier = StdHorzSpacer) - } - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = RowColSpacing, - ) { - LikeReaction( - baseNote = it, - grayTint = MaterialTheme.colorScheme.onSurface, - accountViewModel = accountViewModel, - nav, - ) - } - Spacer(modifier = StdHorzSpacer) - ZapReaction( - baseNote = it, - grayTint = MaterialTheme.colorScheme.onSurface, - accountViewModel = accountViewModel, - nav = nav, - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt index e696c45978..7189bd49ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53 import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -43,11 +44,11 @@ import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel -import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.UsernameDisplay @@ -60,8 +61,8 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags -import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -79,44 +80,14 @@ fun LongLiveActivityChannelHeader( ) { val channelState by observeChannel(baseChannel, accountViewModel) val channel = channelState?.channel as? LiveActivitiesChannel ?: return + val activity = channel.info ?: return + val callbackUri = remember(channel) { channel.toNostrUri() } - Row( - lineModifier, - ) { - val summary = remember(channelState) { channel.summary()?.ifBlank { null } } - - Column( - Modifier.weight(1f), - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - val defaultBackground = MaterialTheme.colorScheme.background - val background = remember { mutableStateOf(defaultBackground) } - - val tags = remember(channelState) { baseChannel.info?.tags?.toImmutableListOfLists() ?: EmptyTagList } - - TranslatableRichTextViewer( - content = summary ?: stringRes(id = R.string.groups_no_descriptor), - canPreview = false, - quotesLeft = 1, - tags = tags, - backgroundColor = background, - id = baseChannel.idHex, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - if (summary != null) { - baseChannel.info?.let { - if (it.hasHashtags()) { - DisplayUncitedHashtags(it, summary, accountViewModel, nav) - } - } - } - } + Row(lineModifier) { + RenderSummary(activity, callbackUri, accountViewModel, nav) } - LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote -> + LoadAddressableNote(channel.address, accountViewModel) { loadingNote -> loadingNote?.let { note -> Row( lineModifier, @@ -197,3 +168,36 @@ fun LongLiveActivityChannelHeader( } } } + +@Composable +private fun RowScope.RenderSummary( + activity: LiveActivitiesEvent, + callbackUri: String, + accountViewModel: AccountViewModel, + nav: INav, +) { + val summary = activity.summary() ?: stringRes(id = R.string.groups_no_descriptor) + + Column(Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + val defaultBackground = MaterialTheme.colorScheme.background + val background = remember { mutableStateOf(defaultBackground) } + + TranslatableRichTextViewer( + content = summary, + canPreview = false, + quotesLeft = 1, + tags = activity.tags.toImmutableListOfLists(), + backgroundColor = background, + id = activity.id, + callbackUri = callbackUri, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (activity.hasHashtags()) { + DisplayUncitedHashtags(activity, summary, callbackUri, accountViewModel, nav) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt index 304e75ac32..699fdb6b5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt @@ -23,10 +23,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53 import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -34,25 +37,51 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.UserPicture +import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.Size34dp import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent @Composable fun ShortLiveActivityChannelHeader( baseChannel: LiveActivitiesChannel, + showFlag: Boolean, accountViewModel: AccountViewModel, nav: INav, - showFlag: Boolean, ) { val channelState by observeChannel(baseChannel, accountViewModel) val channel = channelState?.channel as? LiveActivitiesChannel ?: return + ShortLiveActivityChannelHeader( + name = channel.toBestDisplayName(), + creator = channel.creator, + liveActivitiesEvent = channel.info, + showFlag = showFlag, + accountViewModel = accountViewModel, + nav = nav, + ) +} + +@Composable +fun ShortLiveActivityChannelHeader( + name: String, + creator: User?, + liveActivitiesEvent: LiveActivitiesEvent?, + showFlag: Boolean, + accountViewModel: AccountViewModel, + nav: INav, +) { Row(verticalAlignment = Alignment.CenterVertically) { - channel.creator?.let { + creator?.let { UserPicture( user = it, size = Size34dp, @@ -71,21 +100,60 @@ fun ShortLiveActivityChannelHeader( ) { Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = remember(channelState) { channel.toBestDisplayName() }, + text = name, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } - Row( - modifier = - Modifier - .height(Size35dp) - .padding(start = 5.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - LiveChannelActionOptions(channel, showFlag, accountViewModel, nav) + liveActivitiesEvent?.let { + Row( + modifier = + Modifier + .height(Size35dp) + .padding(start = 5.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + LiveChannelActionOptions(it, showFlag, accountViewModel, nav) + } } } } + +@Composable +fun LiveChannelActionOptions( + activity: LiveActivitiesEvent, + showFlag: Boolean = true, + accountViewModel: AccountViewModel, + nav: INav, +) { + val isLive by remember(activity) { derivedStateOf { activity.isLive() } } + + if (showFlag && isLive) { + LiveFlag() + Spacer(modifier = StdHorzSpacer) + } + + val note = remember(activity) { LocalCache.getAddressableNoteIfExists(activity.address()) } + note?.let { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = RowColSpacing, + ) { + LikeReaction( + baseNote = it, + grayTint = MaterialTheme.colorScheme.onSurface, + accountViewModel = accountViewModel, + nav, + ) + } + Spacer(modifier = StdHorzSpacer) + ZapReaction( + baseNote = it, + grayTint = MaterialTheme.colorScheme.onSurface, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt index 913e504035..475f8301ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt @@ -38,9 +38,9 @@ fun LiveActivityTopBar( title = { ShortLiveActivityChannelHeader( baseChannel = baseChannel, + showFlag = true, accountViewModel = accountViewModel, nav = nav, - showFlag = true, ) }, extendableRow = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 18b0fa8222..152b76e2a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -364,7 +364,6 @@ open class ChannelNewMessageViewModel : message = message.text, pTags = listOfNotNull(replyTo.value?.author), eTags = listOfNotNull(replyTo.value), - channelHex = channel.idHex, dao = accountViewModel, ) tagger.run() @@ -627,7 +626,7 @@ open class ChannelNewMessageViewModel : fun updateZapFromText() { viewModelScope.launch(Dispatchers.Default) { - val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!) + val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!) tagger.run() tagger.pTags?.forEach { taggedUser -> if (!forwardZapTo.items.any { it.key == taggedUser }) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 01f31b15bf..7f0f26e048 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -32,7 +32,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -53,8 +52,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.logTime -import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.PublicChatChannel @@ -62,31 +59,29 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName -import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.note.BlankNote -import com.vitorpamplona.amethyst.ui.note.LoadChannel import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull +import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent -import com.vitorpamplona.amethyst.ui.note.externalLinkForNote import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @Composable @@ -95,18 +90,14 @@ fun ChatroomHeaderCompose( accountViewModel: AccountViewModel, nav: INav, ) { - logTime( - debugMessage = { "ChatroomHeaderCompose " + externalLinkForNote(baseNote) }, - ) { - if (baseNote.event != null) { + if (baseNote.event != null) { + ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav) + } else { + val hasEvent by observeNoteHasEvent(baseNote, accountViewModel) + if (hasEvent) { ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav) } else { - val hasEvent by observeNoteHasEvent(baseNote, accountViewModel) - if (hasEvent) { - ChatroomComposeChannelOrUser(baseNote, accountViewModel, nav) - } else { - BlankNote() - } + BlankNote() } } } @@ -117,80 +108,62 @@ fun ChatroomComposeChannelOrUser( accountViewModel: AccountViewModel, nav: INav, ) { - if (baseNote.event is DraftEvent) { - ObserveDraftEvent(baseNote, accountViewModel) { - val channelHex by remember(it) { derivedStateOf { it.channelHex() } } - - if (channelHex != null) { - ChatroomChannel(channelHex!!, it, accountViewModel, nav) - } else { - ChatroomPrivateMessages(it, accountViewModel, nav) - } + val baseNoteEvent = baseNote.event + if (baseNoteEvent is DraftEvent) { + ObserveDraftEvent(baseNote, accountViewModel) { innerNote -> + ChatroomEntry(innerNote, accountViewModel, nav) } } else { - val channelHex by remember(baseNote) { derivedStateOf { baseNote.channelHex() } } - - if (channelHex != null) { - ChatroomChannel(channelHex!!, baseNote, accountViewModel, nav) - } else { - ChatroomPrivateMessages(baseNote, accountViewModel, nav) - } + ChatroomEntry(baseNote, accountViewModel, nav) } } @Composable -private fun ChatroomPrivateMessages( - baseNote: Note, +private fun ChatroomEntry( + lastMessage: Note, accountViewModel: AccountViewModel, nav: INav, ) { - val userRoom by - remember(baseNote) { - derivedStateOf { - (baseNote.event as? ChatroomKeyable)?.chatroomKey(accountViewModel.userProfile().pubkeyHex) + val baseNoteEvent = lastMessage.event + when (baseNoteEvent) { + is ChannelMessageEvent -> + baseNoteEvent.channelId()?.let { + LoadPublicChatChannel(it, accountViewModel) { channel -> + ChannelRoomCompose(lastMessage, channel, accountViewModel, nav) + } } + is ChannelMetadataEvent -> + baseNoteEvent.channelId()?.let { + LoadPublicChatChannel(it, accountViewModel) { channel -> + ChannelRoomCompose(lastMessage, channel, accountViewModel, nav) + } + } + is ChannelCreateEvent -> + LoadPublicChatChannel(baseNoteEvent.id, accountViewModel) { channel -> + ChannelRoomCompose(lastMessage, channel, accountViewModel, nav) + } + is ChatroomKeyable -> { + val room = baseNoteEvent.chatroomKey(accountViewModel.userProfile().pubkeyHex) + UserRoomCompose(room, lastMessage, accountViewModel, nav) } - - CrossfadeIfEnabled(targetState = userRoom, label = "ChatroomPrivateMessages", accountViewModel = accountViewModel) { room -> - if (room != null) { - UserRoomCompose(baseNote, room, accountViewModel, nav) - } else { - BlankNote() - } - } -} - -@Composable -private fun ChatroomChannel( - channelHex: HexKey, - baseNote: Note, - accountViewModel: AccountViewModel, - nav: INav, -) { - LoadChannel(baseChannelHex = channelHex, accountViewModel) { channel -> - when (channel) { - is PublicChatChannel -> ChannelRoomCompose(baseNote, channel, accountViewModel, nav) - is EphemeralChatChannel -> ChannelRoomCompose(baseNote, channel, accountViewModel, nav) - } + else -> BlankNote() } } @Composable private fun ChannelRoomCompose( - note: Note, + lastMessage: Note, channel: PublicChatChannel, accountViewModel: AccountViewModel, nav: INav, ) { - val authorName by observeUserName(note.author!!, accountViewModel) + val authorName by observeUserName(lastMessage.author!!, accountViewModel) val channelState by observeChannel(channel, accountViewModel) val channelPicture = channelState?.channel?.profilePicture() ?: channel.profilePicture() val channelName = channelState?.channel?.toBestDisplayName() ?: channel.toBestDisplayName() - val noteEvent = note.event - - val route = Route.Channel(channel.idHex) + val noteEvent = lastMessage.event val description = if (noteEvent is ChannelCreateEvent) { @@ -207,54 +180,12 @@ private fun ChannelRoomCompose( channelIdHex = channel.idHex, channelPicture = channelPicture, channelTitle = { modifier -> ChannelTitleWithLabelInfo(channelName, R.string.public_chat, modifier) }, - channelLastTime = note.createdAt(), + channelLastTime = lastMessage.createdAt(), channelLastContent = "$authorName: $description", hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, - onClick = { nav.nav(route) }, - ) -} - -@Composable -private fun ChannelRoomCompose( - note: Note, - channel: EphemeralChatChannel, - accountViewModel: AccountViewModel, - nav: INav, -) { - val authorName by observeUserName(note.author!!, accountViewModel) - val channelState by observeChannel(channel, accountViewModel) - - val relayInfo by loadRelayInfo(channel.roomId.relayUrl, accountViewModel) - - val channelName = channelState?.channel?.toBestDisplayName() ?: channel.toBestDisplayName() - - val noteEvent = note.event - - val route = Route.Channel(channel.idHex) - - val description = - if (noteEvent is ChannelCreateEvent) { - stringRes(R.string.channel_created) - } else if (noteEvent is ChannelMetadataEvent) { - "${stringRes(R.string.channel_information_changed_to)} " - } else { - noteEvent?.content?.take(200) - } - - val lastReadTime by accountViewModel.account.loadLastReadFlow("Channel/${channel.idHex}").collectAsStateWithLifecycle() - - ChannelName( - channelIdHex = channel.idHex, - channelPicture = relayInfo?.icon, - channelTitle = { modifier -> ChannelTitleWithLabelInfo(channelName, R.string.ephemeral_relay_chat, modifier) }, - channelLastTime = note.createdAt(), - channelLastContent = "$authorName: $description", - hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime, - loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, - onClick = { nav.nav(route) }, + onClick = { nav.nav(routeFor(channel)) }, ) } @@ -300,8 +231,8 @@ private fun ChannelTitleWithLabelInfo( @Composable private fun UserRoomCompose( - note: Note, room: ChatroomKey, + lastMessage: Note, accountViewModel: AccountViewModel, nav: INav, ) { @@ -315,10 +246,10 @@ private fun UserRoomCompose( }, firstRow = { RoomNameDisplay(room, Modifier.weight(1f), accountViewModel) - TimeAgo(note.createdAt()) + TimeAgo(lastMessage.createdAt()) }, secondRow = { - LoadDecryptedContentOrNull(note, accountViewModel) { content -> + LoadDecryptedContentOrNull(lastMessage, accountViewModel) { content -> if (content != null) { Text( content, @@ -340,7 +271,7 @@ private fun UserRoomCompose( } val lastReadTime by accountViewModel.account.loadLastReadFlow("Room/${room.hashCode()}").collectAsStateWithLifecycle() - if ((note.createdAt() ?: Long.MIN_VALUE) > lastReadTime) { + if ((lastMessage.createdAt() ?: Long.MIN_VALUE) > lastReadTime) { NewItemsBubble() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 83f10b485f..87c05338d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -58,7 +58,7 @@ class ChatroomListKnownFeedFilter( val publicChannels = account .publicChatList.flow.value - .mapNotNull { LocalCache.getChannelIfExists(it.eventId) } + .mapNotNull { LocalCache.getPublicChatChannelIfExists(it.eventId) } .mapNotNull { it -> it.notes .filter { key, it -> account.isAcceptable(it) && it.event != null } @@ -69,7 +69,7 @@ class ChatroomListKnownFeedFilter( val ephemeralChats = account .ephemeralChatList.liveEphemeralChatList.value - .mapNotNull { LocalCache.getChannelIfExists(it) } + .mapNotNull { LocalCache.getEphemeralChatChannelIfExists(it) } .mapNotNull { it -> it.notes .filter { key, it -> account.isAcceptable(it) && it.event != null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt index e92020f0d1..e836d3d95b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterFollowingPublicChats.kt @@ -39,7 +39,7 @@ fun filterFollowingPublicChats( mapOfSet { followingChannels.forEach { channelId -> val relays = - LocalCache.getChannelIfExists(channelId)?.relays() + LocalCache.getPublicChatChannelIfExists(channelId)?.relays() ?: LocalCache.relayHints.hintsForEvent(channelId).ifEmpty { null } ?: Constants.eventFinderRelays diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt index a7723ac357..0c9c831418 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FilterLastMessageFollowingPublicChats.kt @@ -40,7 +40,7 @@ fun filterLastMessageFollowingPublicChats( mapOfSet { followingChannels.forEach { channelId -> val relays = - LocalCache.getChannelIfExists(channelId)?.relays() + LocalCache.getPublicChatChannelIfExists(channelId)?.relays() ?: LocalCache.relayHints.hintsForEvent(channelId).ifEmpty { null } ?: Constants.eventFinderRelays diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 4aa5ffeb7d..46e48e4011 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.amethyst.ui.navigation.MainTopBar import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.Chatroom -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelFabColumn import com.vitorpamplona.amethyst.ui.theme.Size20dp @@ -62,13 +62,9 @@ fun MessagesTwoPane( val strategy = remember { if (widthSizeClass == WindowWidthSizeClass.Expanded) { - HorizontalTwoPaneStrategy( - splitFraction = 1f / 3f, - ) + HorizontalTwoPaneStrategy(splitFraction = 1f / 3f) } else { - HorizontalTwoPaneStrategy( - splitFraction = 1f / 2.5f, - ) + HorizontalTwoPaneStrategy(splitFraction = 1f / 2.5f) } } @@ -117,8 +113,8 @@ fun MessagesTwoPane( ) } - if (it is Route.Channel) { - ChannelView( + if (it is Route.PublicChatChannel) { + PublicChatChannelView( channelId = it.id, accountViewModel = accountViewModel, nav = nav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt index e7dd5067a2..693c2ff3b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt @@ -38,18 +38,18 @@ class TwoPaneNav( val innerNav = mutableStateOf(null) override fun nav(route: Route) { - if (route is Route.Room || route is Route.Channel) { + if (route is Route.Room || route is Route.PublicChatChannel) { innerNav.value = route } else { nav.nav(route) } } - override fun nav(routeMaker: suspend () -> Route?) { + override fun nav(computeRoute: suspend () -> Route?) { scope.launch(Dispatchers.Default) { - val route = routeMaker() + val route = computeRoute() if (route != null) { - if (route is Route.Room || route is Route.Channel) { + if (route is Route.Room || route is Route.PublicChatChannel) { innerNav.value = route } else { nav.nav(route) @@ -68,9 +68,9 @@ class TwoPaneNav( override fun popUpTo( route: Route, - upToClass: KClass, + klass: KClass, ) { - nav.popUpTo(route, upToClass) + nav.popUpTo(route, klass) } override fun closeDrawer() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt index a3ae7c7b8d..72b9beb24d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.RenderLongFormThumb -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.RenderChannelThumb +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.RenderPublicChatChannelThumb import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.RenderFollowSetThumb import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.RenderLiveActivityThumb import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.RenderCommunitiesThumb @@ -203,7 +203,7 @@ private fun RenderNoteRow( when (baseNote.event) { is LiveActivitiesEvent -> RenderLiveActivityThumb(baseNote, accountViewModel, nav) is CommunityDefinitionEvent -> RenderCommunitiesThumb(baseNote, accountViewModel, nav) - is ChannelCreateEvent -> RenderChannelThumb(baseNote, accountViewModel, nav) + is ChannelCreateEvent -> RenderPublicChatChannelThumb(baseNote, accountViewModel, nav) is AppDefinitionEvent -> RenderContentDVMThumb(baseNote, accountViewModel, nav) is FollowListEvent -> RenderFollowSetThumb(baseNote, accountViewModel, nav) is LongTextNoteEvent -> RenderLongFormThumb(baseNote, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt index 86c065b858..122863ba5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt @@ -39,7 +39,7 @@ import kotlinx.coroutines.launch class DiscoveryFollowsDiscoverySubAssembler1( client: NostrClient, allKeys: () -> Set, -) : PerUserAndFollowListEoseManager(client, allKeys) { +) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: DiscoveryQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt index 4a4cd20686..a8839c5e81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.launch class DiscoveryFollowsDiscoverySubAssembler2( client: NostrClient, allKeys: () -> Set, -) : PerUserAndFollowListEoseManager(client, allKeys) { +) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: DiscoveryQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt index 97d6941913..2660a51aa0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.launch class DiscoveryFollowsDiscoverySubAssembler3( client: NostrClient, allKeys: () -> Set, -) : PerUserAndFollowListEoseManager(client, allKeys) { +) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: DiscoveryQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt index f43b0e5a14..039fddc9a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt @@ -74,7 +74,7 @@ open class DiscoverChatFeedFilter( // note event here will never be null val noteEvent = note.event if (noteEvent is ChannelCreateEvent && params.match(noteEvent)) { - if ((LocalCache.getChannelIfExists(noteEvent.id)?.notes?.size() ?: 0) > 0) { + if ((LocalCache.getPublicChatChannelIfExists(noteEvent.id)?.notes?.size() ?: 0) > 0) { note } else { null @@ -86,7 +86,7 @@ open class DiscoverChatFeedFilter( if (channel != null && (channelEvent == null || (channelEvent is ChannelCreateEvent && params.match(channelEvent))) ) { - if ((LocalCache.getChannelIfExists(channel.idHex)?.notes?.size() ?: 0) > 0) { + if ((LocalCache.getPublicChatChannelIfExists(channel.idHex)?.notes?.size() ?: 0) > 0) { channel } else { null @@ -103,7 +103,7 @@ open class DiscoverChatFeedFilter( override fun sort(collection: Set): List { val lastNote = collection.associateWith { note -> - LocalCache.getChannelIfExists(note.idHex)?.lastNoteCreatedAt ?: 0 + LocalCache.getPublicChatChannelIfExists(note.idHex)?.lastNoteCreatedAt ?: 0 } return collection diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderChannelThumb.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 70249b8dcd..d1e4794050 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -41,9 +41,9 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder +import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter @@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.note.LikeReaction -import com.vitorpamplona.amethyst.ui.note.LoadChannel +import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists @@ -73,22 +73,22 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable -fun RenderChannelThumb( +fun RenderPublicChatChannelThumb( baseNote: Note, accountViewModel: AccountViewModel, nav: INav, ) { val noteEvent = baseNote.event as? ChannelCreateEvent ?: return - LoadChannel(baseChannelHex = baseNote.idHex, accountViewModel) { - RenderChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav) + LoadPublicChatChannel(baseNote.idHex, accountViewModel) { + RenderPublicChatChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav) } } @Composable -fun RenderChannelThumb( +fun RenderPublicChatChannelThumb( baseNote: Note, - channel: Channel, + channel: PublicChatChannel, accountViewModel: AccountViewModel, nav: INav, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt index 09660985be..a67cb2f4ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt @@ -47,7 +47,7 @@ open class DiscoverLiveFeedFilter( followList() == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) override fun feed(): List { - val allChannelNotes = LocalCache.liveChatChannels.mapNotNull { _, channel -> LocalCache.getNoteIfExists(channel.idHex) } + val allChannelNotes = LocalCache.liveChatChannels.mapNotNull { _, channel -> LocalCache.getAddressableNoteIfExists(channel.address) } val allMessageNotes = LocalCache.liveChatChannels.map { _, channel -> channel.notes.filter { key, it -> it.event is LiveActivitiesEvent } }.flatten() val notes = innerApplyFilter(allChannelNotes + allMessageNotes) @@ -94,7 +94,7 @@ open class DiscoverLiveFeedFilter( return collection .sortedWith( compareBy( - { convertStatusToOrder((it.event as? LiveActivitiesEvent)?.status()) }, + { convertStatusToOrder((it.event as? LiveActivitiesEvent)?.statusEnum()) }, { participantCounts[it] }, { allParticipants[it] }, { (it.event as? LiveActivitiesEvent)?.starts() ?: it.createdAt() }, @@ -103,11 +103,11 @@ open class DiscoverLiveFeedFilter( ).reversed() } - fun convertStatusToOrder(status: String?): Int = + fun convertStatusToOrder(status: StatusTag.STATUS?): Int = when (status) { - StatusTag.STATUS.LIVE.code -> 2 - StatusTag.STATUS.PLANNED.code -> 1 - StatusTag.STATUS.ENDED.code -> 0 + StatusTag.STATUS.LIVE -> 2 + StatusTag.STATUS.PLANNED -> 1 + StatusTag.STATUS.ENDED -> 0 else -> 0 } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 775ca02efb..897cea356b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -54,9 +54,10 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery +import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.EndedFlag +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivitiesChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.OfflineFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ScheduledFlag @@ -64,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.CheckIfVideoIsOnline import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag @@ -75,13 +77,14 @@ import kotlinx.coroutines.launch @Immutable data class LiveActivityCard( + val id: Address?, val name: String, val cover: String?, val media: String?, val subject: String?, val content: String?, val participants: ImmutableList, - val status: String?, + val status: StatusTag.STATUS?, val starts: Long?, ) @@ -95,13 +98,14 @@ fun RenderLiveActivityThumb( val noteEvent = it.event as? LiveActivitiesEvent LiveActivityCard( + id = noteEvent?.address(), name = noteEvent?.dTag() ?: "", cover = noteEvent?.image()?.ifBlank { null }, media = noteEvent?.streaming(), subject = noteEvent?.title()?.ifBlank { null }, content = noteEvent?.summary(), participants = noteEvent?.participants()?.toImmutableList() ?: persistentListOf(), - status = noteEvent?.status(), + status = noteEvent?.statusEnum(), starts = noteEvent?.starts(), ) } @@ -146,7 +150,7 @@ fun RenderLiveActivityThumb( Box(Modifier.padding(10.dp)) { CrossfadeIfEnabled(targetState = card.status, label = "RenderLiveActivityThumb", accountViewModel = accountViewModel) { when (it) { - StatusTag.STATUS.LIVE.code -> { + StatusTag.STATUS.LIVE -> { val url = card.media if (url.isNullOrBlank()) { LiveFlag() @@ -160,10 +164,10 @@ fun RenderLiveActivityThumb( } } } - StatusTag.STATUS.ENDED.code -> { + StatusTag.STATUS.ENDED -> { EndedFlag() } - StatusTag.STATUS.PLANNED.code -> { + StatusTag.STATUS.PLANNED -> { ScheduledFlag(card.starts) } else -> { @@ -188,15 +192,19 @@ fun RenderLiveActivityThumb( Spacer(modifier = DoubleVertSpacer) - ChannelHeader( - channelHex = baseNote.idHex, - showVideo = false, - showFlag = false, - sendToChannel = true, - modifier = Modifier, - accountViewModel = accountViewModel, - nav = nav, - ) + baseNote.address()?.let { + LoadLiveActivityChannel(it, accountViewModel) { + LiveActivitiesChannelHeader( + baseChannel = it, + showVideo = false, + showFlag = false, + sendToChannel = true, + modifier = Modifier, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 3648ad00c3..cce40d4b03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -596,7 +596,7 @@ open class NewProductViewModel : override fun updateZapFromText() { viewModelScope.launch(Dispatchers.Default) { - val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!) + val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!) tagger.run() tagger.pTags?.forEach { taggedUser -> if (!forwardZapTo.value.items.any { it.key == taggedUser }) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt index bd4d569244..1418f06432 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFeedFilterSubAssembler.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class GeoHashFeedFilterSubAssembler( client: NostrClient, allKeys: () -> Set, -) : PerUniqueIdEoseManager(client, allKeys) { +) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: GeohashQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt index 81668475c7..b59790f2c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFeedFilterSubAssembler.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class HashtagFeedFilterSubAssembler( client: NostrClient, allKeys: () -> Set, -) : PerUniqueIdEoseManager(client, allKeys) { +) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: HashtagQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 87dd27e364..65e81afb7e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -56,7 +56,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AROUND_ME -import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled @@ -343,10 +342,8 @@ fun DisplayLiveBubbles( val feed by liveFeed.feed.collectAsStateWithLifecycle() LazyRow(HorzPadding, horizontalArrangement = spacedBy(Size5dp)) { - itemsIndexed(feed.list, key = { _, item -> item.idHex }) { _, item -> - when (item) { - is EphemeralChatChannel -> RenderEphemeralBubble(item, accountViewModel, nav) - } + itemsIndexed(feed.list, key = { _, item -> item.roomId.toKey() }) { _, item -> + RenderEphemeralBubble(item, accountViewModel, nav) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index af371a87cd..b0a5af92ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -516,7 +516,6 @@ open class ShortNotePostViewModel : message.text, pTags, eTags, - originalNote?.channelHex(), accountViewModel!!, ) tagger.run() @@ -865,7 +864,7 @@ open class ShortNotePostViewModel : override fun updateZapFromText() { viewModelScope.launch(Dispatchers.Default) { val tagger = - NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!) + NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!) tagger.run() tagger.pTags?.forEach { taggedUser -> if (!forwardZapTo.value.items.any { it.key == taggedUser }) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 7005e70167..ae1a0f7a72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -38,7 +37,7 @@ import kotlin.collections.toSet class HomeLiveFilter( val account: Account, -) : AdditiveComplexFeedFilter() { +) : AdditiveComplexFeedFilter() { override fun feedKey(): String = account.userProfile().pubkeyHex override fun showHiddenKey(): Boolean = false @@ -51,7 +50,7 @@ class HomeLiveFilter( fun limitTime() = TimeUtils.fifteenMinutesAgo() - override fun feed(): List { + override fun feed(): List { val filterParams = buildFilterParams(account) val fiveMinsAgo = limitTime() @@ -74,9 +73,9 @@ class HomeLiveFilter( }.isNotEmpty() override fun updateListWith( - oldList: List, + oldList: List, newItems: Set, - ): List { + ): List { val fiveMinsAgo = limitTime() val revisedOldList = @@ -91,7 +90,7 @@ class HomeLiveFilter( .mapNotNull { val room = (it.event as? EphemeralChatEvent)?.roomId() if (room != null) { - LocalCache.getChannelIfExists(room) + LocalCache.getEphemeralChatChannelIfExists(room) } else { null } @@ -124,7 +123,7 @@ class HomeLiveFilter( filterParams.match(noteEvent, note.relays) } - fun sort(collection: Set): List { + fun sort(collection: Set): List { val topFilter = account.liveDiscoveryFollowLists.value val topFilterAuthors = when (topFilter) { @@ -141,18 +140,17 @@ class HomeLiveFilter( collection.associate { it to followsThatParticipateOn(it, followingKeySet) } return collection.sortedWith( - compareByDescending { followCounts[it] } - .thenByDescending { it.lastNoteCreatedAt } - .thenBy { it.idHex }, + compareByDescending { followCounts[it] } + .thenByDescending { it.lastNoteCreatedAt } + .thenBy { it.roomId.id } + .thenBy { it.roomId.relayUrl }, ) } fun followsThatParticipateOn( - channel: Channel, + channel: EphemeralChatChannel, followingSet: Set?, ): Int { - if (channel == null) return 0 - var count = 0 channel.notes.forEach { key, value -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt index 136751a4dc..755b551348 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt @@ -49,7 +49,7 @@ import kotlinx.coroutines.launch class HomeOutboxEventsEoseManager( client: NostrClient, allKeys: () -> Set, -) : PerUserAndFollowListEoseManager(client, allKeys) { +) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: HomeQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index bc8fd28747..0af6169f12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -72,7 +72,7 @@ class CardFeedContentState( val scrollToTop = _scrollToTop.asStateFlow() var scrolltoTopPending = false - private var lastFeedKey: String? = null + private var lastFeedKey: Any? = null override val isRefreshing: MutableState = mutableStateOf(false) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt index 9cb0b02b17..87dc2c266a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt @@ -29,33 +29,21 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote +import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Nav import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent -import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Composable @@ -66,24 +54,15 @@ fun LoadRedirectScreen( ) { if (eventId == null) return - var noteBase by remember { mutableStateOf(null) } - - LaunchedEffect(eventId) { - launch(Dispatchers.IO) { - val newNoteBase = LocalCache.checkGetOrCreateNote(eventId) - if (newNoteBase != noteBase) { - noteBase = newNoteBase - } + LoadNote(eventId, accountViewModel) { note -> + note?.let { + LoadRedirectScreen( + baseNote = note, + accountViewModel = accountViewModel, + nav = nav, + ) } } - - noteBase?.let { - LoadRedirectScreen( - baseNote = it, - accountViewModel = accountViewModel, - nav = nav, - ) - } } @Composable @@ -95,11 +74,13 @@ fun LoadRedirectScreen( val noteState by observeNote(baseNote, accountViewModel) LaunchedEffect(key1 = noteState) { - val note = noteState?.note ?: return@LaunchedEffect - val event = note.event - + val event = noteState.note.event if (event != null) { - withContext(Dispatchers.IO) { redirect(event, accountViewModel, nav) } + withContext(Dispatchers.IO) { + routeFor(event, accountViewModel.userProfile())?.let { route -> + nav.popUpTo(route, Route.EventRedirect::class) + } + } } } @@ -111,62 +92,3 @@ fun LoadRedirectScreen( Text(stringRes(R.string.looking_for_event, baseNote.idHex)) } } - -fun redirect( - eventId: HexKey, - accountViewModel: AccountViewModel, - nav: INav, -) { - LocalCache.getNoteIfExists(eventId)?.event?.let { - redirect(it, accountViewModel, nav) - } -} - -fun redirect( - event: Event, - accountViewModel: AccountViewModel, - nav: INav, -) { - val channelHex = - if ( - event is ChannelMessageEvent || - event is ChannelMetadataEvent || - event is ChannelCreateEvent || - event is LiveActivitiesChatMessageEvent || - event is LiveActivitiesEvent - ) { - (event as? ChannelMessageEvent)?.channelId() - ?: (event as? ChannelMetadataEvent)?.channelId() - ?: (event as? ChannelCreateEvent)?.id - ?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag() - ?: (event as? LiveActivitiesEvent)?.aTag()?.toTag() - } else { - null - } - - if (event is GiftWrapEvent) { - event.innerEventId?.let { - redirect(it, accountViewModel, nav) - } ?: run { - accountViewModel.unwrap(event) { redirect(it, accountViewModel, nav) } - } - } else if (event is SealedRumorEvent) { - event.innerEventId?.let { - redirect(it, accountViewModel, nav) - } ?: run { - accountViewModel.unseal(event) { redirect(it, accountViewModel, nav) } - } - } else { - if (event is ChannelCreateEvent) { - nav.popUpTo(Route.Channel(event.id), Route.EventRedirect::class) - } else if (event is ChatroomKeyable) { - val withKey = event.chatroomKey(accountViewModel.userProfile().pubkeyHex) - accountViewModel.userProfile().createChatroom(withKey) - nav.popUpTo(Route.Room(withKey.hashCode()), Route.EventRedirect::class) - } else if (channelHex != null) { - nav.popUpTo(Route.Channel(channelHex), Route.EventRedirect::class) - } else { - nav.popUpTo(Route.Note(event.id), Route.EventRedirect::class) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt index 045d583c46..b6241290c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.focus.FocusRequester import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.searchCommand.SearchQueryState @@ -71,36 +70,58 @@ class SearchBarViewModel( val searchDataSourceState = SearchQueryState(MutableStateFlow(searchValue), account) val searchResultsUsers = - combine(searchValueFlow.debounce(100), invalidations.debounce(100)) { term, version -> - logTime("SearchBarViewModel findUsersStartingWith") { - LocalCache.findUsersStartingWith(term, account) - } + combine( + searchValueFlow.debounce(100), + invalidations.debounce(100), + ) { term, version -> + LocalCache.findUsersStartingWith(term, account) }.flowOn(Dispatchers.Default) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) val searchResultsNotes = - combine(searchValueFlow.debounce(100), invalidations) { term, version -> - logTime("SearchBarViewModel findNotesStartingWith") { - LocalCache - .findNotesStartingWith(term, account.hiddenUsers) - .sortedWith(DefaultFeedOrder) - } + combine( + searchValueFlow.debounce(100), + invalidations, + ) { term, version -> + LocalCache + .findNotesStartingWith(term, account.hiddenUsers) + .sortedWith(DefaultFeedOrder) }.flowOn(Dispatchers.Default) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) - val searchResultsChannels = - combine(searchValueFlow.debounce(100), invalidations) { term, version -> - logTime("SearchBarViewModel findChannelsStartingWith") { - LocalCache.findChannelsStartingWith(term) - } + val searchResultsPublicChatChannels = + combine( + searchValueFlow.debounce(100), + invalidations, + ) { term, version -> + LocalCache.findPublicChatChannelsStartingWith(term) + }.flowOn(Dispatchers.Default) + .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) + + val searchResultsEphemeralChannels = + combine( + searchValueFlow.debounce(100), + invalidations, + ) { term, version -> + LocalCache.findEphemeralChatChannelsStartingWith(term) + }.flowOn(Dispatchers.Default) + .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) + + val searchResultsLiveActivityChannels = + combine( + searchValueFlow.debounce(100), + invalidations, + ) { term, version -> + LocalCache.findLiveActivityChannelsStartingWith(term) }.flowOn(Dispatchers.Default) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) val hashtagResults = - combine(searchValueFlow.debounce(100), invalidations) { term, version -> - logTime("SearchBarViewModel findHashtags") { - findHashtags(term) - } + combine( + searchValueFlow.debounce(100), + invalidations, + ) { term, version -> + findHashtags(term) }.flowOn(Dispatchers.Default) .stateIn(viewModelScope, WhileSubscribed(5000), emptyList()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 4f150f9f24..99d8ca6009 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routeFor import com.vitorpamplona.amethyst.ui.note.ClearTextIcon import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.SearchIcon @@ -230,7 +231,9 @@ private fun DisplaySearchResults( val hashTags by searchBarViewModel.hashtagResults.collectAsStateWithLifecycle() val users by searchBarViewModel.searchResultsUsers.collectAsStateWithLifecycle() - val channels by searchBarViewModel.searchResultsChannels.collectAsStateWithLifecycle() + val publicChatChannels by searchBarViewModel.searchResultsPublicChatChannels.collectAsStateWithLifecycle() + val ephemeralChannels by searchBarViewModel.searchResultsEphemeralChannels.collectAsStateWithLifecycle() + val liveActivityChannels by searchBarViewModel.searchResultsLiveActivityChannels.collectAsStateWithLifecycle() val notes by searchBarViewModel.searchResultsNotes.collectAsStateWithLifecycle() LazyColumn( @@ -262,8 +265,8 @@ private fun DisplaySearchResults( } itemsIndexed( - channels, - key = { _, item -> "c" + item.idHex }, + publicChatChannels, + key = { _, item -> "public" + item.idHex }, ) { _, item -> ChannelName( channelIdHex = item.idHex, @@ -279,7 +282,61 @@ private fun DisplaySearchResults( hasNewMessages = false, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, - onClick = { nav.nav(Route.Channel(item.idHex)) }, + onClick = { nav.nav(routeFor(item)) }, + ) + + HorizontalDivider( + modifier = StdTopPadding, + thickness = DividerThickness, + ) + } + + itemsIndexed( + ephemeralChannels, + key = { _, item -> "ephem" + item.roomId.toKey() }, + ) { _, item -> + ChannelName( + channelIdHex = item.roomId.toKey(), + channelPicture = item.profilePicture(), + channelTitle = { + Text( + item.toBestDisplayName(), + fontWeight = FontWeight.Bold, + ) + }, + channelLastTime = null, + channelLastContent = item.summary(), + hasNewMessages = false, + loadProfilePicture = accountViewModel.settings.showProfilePictures.value, + loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + onClick = { nav.nav(routeFor(item)) }, + ) + + HorizontalDivider( + modifier = StdTopPadding, + thickness = DividerThickness, + ) + } + + itemsIndexed( + liveActivityChannels, + key = { _, item -> "live" + item.address.toValue() }, + ) { _, item -> + ChannelName( + channelIdHex = item.address.toValue(), + channelPicture = item.profilePicture(), + channelTitle = { + Text( + item.toBestDisplayName(), + fontWeight = FontWeight.Bold, + ) + }, + channelLastTime = null, + channelLastContent = item.summary(), + hasNewMessages = false, + loadProfilePicture = accountViewModel.settings.showProfilePictures.value, + loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + onClick = { nav.nav(routeFor(item)) }, ) HorizontalDivider( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 3724ba5d65..e97ca99502 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -153,7 +153,7 @@ import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.LevelFeedViewModel @@ -525,9 +525,8 @@ private fun FullBleedNoteCompose( (noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseNote.channelHex() != null ) { - ChannelHeader( + PublicChatChannelHeader( channelHex = baseNote.channelHex()!!, - showVideo = true, sendToChannel = true, accountViewModel = accountViewModel, nav = nav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt index 5018c949a3..cc3e8a12c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadEventLoaderSubAssembler.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -39,7 +40,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ThreadEventLoaderSubAssembler( client: NostrClient, allKeys: () -> Set, -) : PerUniqueIdEoseManager(client, allKeys, invalidateAfterEose = true) { +) : PerUniqueIdEoseManager(client, allKeys, invalidateAfterEose = true) { override fun updateFilter( key: ThreadQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt index fa3efb51bd..f65a9d330b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/ThreadFilterSubAssembler.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.ThreadAssembler import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadQueryState +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -36,7 +37,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter class ThreadFilterSubAssembler( client: NostrClient, allKeys: () -> Set, -) : PerUniqueIdEoseManager(client, allKeys) { +) : PerUniqueIdEoseManager(client, allKeys) { override fun updateFilter( key: ThreadQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt index 9d0311ad3a..774aebe750 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt @@ -52,7 +52,7 @@ import kotlinx.coroutines.launch class VideoOutboxEventsFilterSubAssembler( client: NostrClient, allKeys: () -> Set, -) : PerUserAndFollowListEoseManager(client, allKeys) { +) : PerUserAndFollowListEoseManager(client, allKeys) { override fun updateFilter( key: VideoQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 35231bc118..317d97ff70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn @@ -331,3 +332,13 @@ val LargeRelayIconModifier = Modifier .size(Size55dp) .clip(shape = CircleShape) + +val FollowSetImageModifier = + Modifier + .fillMaxWidth() + .aspectRatio(ratio = 21f / 9f) + .clip(QuoteBorder) + +val SimpleImageBorder = Modifier.fillMaxSize().clip(QuoteBorder) + +val SimpleHeaderImage = Modifier.fillMaxWidth().heightIn(max = 200.dp) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt index 293865dc34..6b6c0fad80 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt @@ -90,6 +90,10 @@ class LiveActivitiesEvent( fun status() = checkStatus(tags.firstNotNullOfOrNull(StatusTag::parse)) + fun statusEnum() = checkStatusEnum(tags.firstNotNullOfOrNull(StatusTag::parseEnum)) + + fun isLive() = statusEnum() == StatusTag.STATUS.LIVE + fun currentParticipants() = tags.firstNotNullOfOrNull(CurrentParticipantsTag::parse) fun totalParticipants() = tags.firstNotNullOfOrNull(TotalParticipantsTag::parse) @@ -113,6 +117,13 @@ class LiveActivitiesEvent( eventStatus } + fun checkStatusEnum(eventStatus: StatusTag.STATUS?): StatusTag.STATUS? = + if (eventStatus == StatusTag.STATUS.LIVE && createdAt < TimeUtils.eightHoursAgo()) { + StatusTag.STATUS.ENDED + } else { + eventStatus + } + fun participantsIntersect(keySet: Set): Boolean = keySet.contains(pubKey) || tags.any(ParticipantTag::isIn, keySet) companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StatusTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StatusTag.kt index f788aa3186..37f65ba403 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StatusTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/tags/StatusTag.kt @@ -33,6 +33,16 @@ class StatusTag { ; fun toTagArray() = assemble(this) + + companion object { + fun parse(code: String): STATUS? = + when (code) { + LIVE.code -> LIVE + PLANNED.code -> PLANNED + ENDED.code -> ENDED + else -> null + } + } } companion object { @@ -46,6 +56,14 @@ class StatusTag { return tag[1] } + @JvmStatic + fun parseEnum(tag: Array): STATUS? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return STATUS.parse(tag[1]) + } + @JvmStatic fun assemble(status: STATUS) = arrayOf(TAG_NAME, status.code) } From ee8ffcd03d4590b079f153ce2f4f39610199c06c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 11:08:39 -0400 Subject: [PATCH 070/185] Fixes some borders of preview cards --- .../amethyst/ui/note/types/LongForm.kt | 29 ++++--------------- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 2 +- .../vitorpamplona/amethyst/ui/theme/Theme.kt | 2 +- 3 files changed, 7 insertions(+), 26 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt index d029ba7b68..bbeded0da7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.types -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -30,7 +29,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow @@ -43,11 +41,8 @@ import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.QuoteBorder -import com.vitorpamplona.amethyst.ui.theme.SimpleImageBorder -import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent @Composable @@ -74,34 +69,20 @@ fun LongFormHeader( noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null } } - Column( - modifier = - Modifier - .padding(top = Size5dp) - .clip(shape = QuoteBorder) - .border( - 1.dp, - MaterialTheme.colorScheme.subtleBorder, - QuoteBorder, - ), - ) { + Column(MaterialTheme.colorScheme.replyModifier) { image?.let { MyAsyncImage( imageUrl = it, - contentDescription = - stringRes( - R.string.preview_card_image_for, - it, - ), + contentDescription = stringRes(R.string.preview_card_image_for, it), contentScale = ContentScale.FillWidth, - mainImageModifier = SimpleImageBorder, + mainImageModifier = Modifier.fillMaxWidth(), loadedImageModifier = Modifier, accountViewModel = accountViewModel, onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel) }, onError = { DefaultImageHeader(note, accountViewModel) }, ) } ?: run { - DefaultImageHeader(note, accountViewModel, SimpleImageBorder) + DefaultImageHeader(note, accountViewModel, Modifier.fillMaxWidth()) } title?.let { Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 317d97ff70..b21b0bc592 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -336,8 +336,8 @@ val LargeRelayIconModifier = val FollowSetImageModifier = Modifier .fillMaxWidth() - .aspectRatio(ratio = 21f / 9f) .clip(QuoteBorder) + .aspectRatio(ratio = 21f / 9f) val SimpleImageBorder = Modifier.fillMaxSize().clip(QuoteBorder) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt index 39e062d22f..2258053c4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Theme.kt @@ -167,7 +167,7 @@ val DarkReplyBorderModifier = val LightReplyBorderModifier = Modifier - .padding(top = 2.dp, bottom = 0.dp, start = 0.dp, end = 0.dp) + .padding(top = 5.dp) .fillMaxWidth() .clip(shape = QuoteBorder) .border(1.dp, LightSubtleBorder, QuoteBorder) From ca41d4e10a907f186b1620fe338a41fc34fd92c3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 11:13:52 -0400 Subject: [PATCH 071/185] Refactoring --- .../amethyst/ui/screen/loggedIn/video/VideoScreen.kt | 5 ++--- .../main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index e925c9be17..a699dfc1d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -90,6 +89,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilte import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.AuthorInfoVideoFeed import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.HalfFeedPadding import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.Size35Modifier @@ -281,8 +281,7 @@ private fun RenderVideoOrPictureNote( val noteEvent = remember { note.event } if (noteEvent is PictureEvent) { val backgroundColor = remember { mutableStateOf(Color.Transparent) } - - PictureDisplay(note, false, ContentScale.Fit, PaddingValues(5.dp), backgroundColor, accountViewModel, nav) + PictureDisplay(note, false, ContentScale.Fit, HalfFeedPadding, backgroundColor, accountViewModel, nav) } else if (noteEvent is FileHeaderEvent) { FileHeaderDisplay(note, false, ContentScale.Fit, accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index b21b0bc592..23752b92e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -219,6 +219,7 @@ val EditFieldModifier = val EditFieldTrailingIconModifier = Modifier.padding(start = 5.dp, end = 0.dp) val ZeroPadding = PaddingValues(0.dp) +val HalfFeedPadding = PaddingValues(5.dp) val FeedPadding = PaddingValues(top = 10.dp, bottom = 10.dp) val ButtonPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp) From 8dbb1a889e52e2abf1ceee2e57abdb6d2e099897 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 11:44:21 -0400 Subject: [PATCH 072/185] Avoids displaying loading symbol on our cached AsyncImage implementation --- .../amethyst/ui/components/MyAsyncImage.kt | 11 +-- .../loggedIn/discover/nip90DVMs/DVMCard.kt | 42 +++++----- .../dvms/DvmContentDiscoveryScreen.kt | 84 +++++++++++++------ .../ui/screen/loggedIn/dvms/DvmTopBar.kt | 40 +++++---- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 3 + 5 files changed, 115 insertions(+), 65 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt index 3d35c44631..ef08d9374d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt @@ -82,8 +82,10 @@ fun MyAsyncImage( Box(contentAlignment = Alignment.Center) { if (onLoadingBackground != null) { onLoadingBackground() + LoadingAnimation(Size40dp, Size6dp) + } else { + DisplayUrlWithLoadingSymbol(imageUrl) } - DisplayUrlWithLoadingSymbol(imageUrl) } } } @@ -96,12 +98,11 @@ fun MyAsyncImage( } } else { Box(loadedImageModifier, contentAlignment = Alignment.Center) { - if (onLoadingBackground != null) { - onLoadingBackground() - } - DisplayUrlWithLoadingSymbol(imageUrl) + onError() } } + } else { + ClickableUrl(urlText = imageUrl, url = imageUrl) } } is AsyncImagePainter.State.Success -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt index 3f5fda703c..cbf68e2dd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs +import android.R.attr.author import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -35,7 +35,6 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment.Companion.BottomStart import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight @@ -44,10 +43,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription +import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout +import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction @@ -55,13 +55,14 @@ import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.observeAppDefinition import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding -import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.RowColSpacing5dp +import com.vitorpamplona.amethyst.ui.theme.SimpleImageBorder import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.nip05 +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError @Immutable data class DVMCard( @@ -87,25 +88,28 @@ fun RenderContentDVMThumb( onImage = { card.cover?.let { Box(contentAlignment = BottomStart) { - AsyncImage( - model = it, - contentDescription = null, + MyAsyncImage( + imageUrl = it, + contentDescription = card.name, contentScale = ContentScale.Crop, - modifier = - Modifier - .fillMaxSize() - .clip(QuoteBorder), + mainImageModifier = Modifier, + loadedImageModifier = SimpleImageBorder, + accountViewModel = accountViewModel, + onLoadingBackground = { + baseNote.author?.let { author -> + BannerImage(author, SimpleImageBorder, accountViewModel) + } + }, + onError = { + baseNote.author?.let { author -> + BannerImage(author, SimpleImageBorder, accountViewModel) + } + }, ) } } ?: run { - baseNote.author?.let { - BannerImage( - it, - Modifier - .fillMaxSize() - .clip(QuoteBorder), - accountViewModel, - ) + baseNote.author?.let { author -> + BannerImage(author, SimpleImageBorder, accountViewModel) } } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 4f66b17bd3..b9509a2dd4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -44,8 +45,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.BottomStart import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext @@ -56,7 +57,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User @@ -66,17 +66,18 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup import com.vitorpamplona.amethyst.ui.note.ZapIcon import com.vitorpamplona.amethyst.ui.note.ZappedIcon +import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.note.elements.customZapClick import com.vitorpamplona.amethyst.ui.note.payViaIntent import com.vitorpamplona.amethyst.ui.screen.RenderFeedState @@ -87,10 +88,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms.dal.NIP90ContentDiscov import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp -import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.SimpleImage75Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp -import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent @@ -324,16 +324,33 @@ fun FeedDVM( val card = observeAppDefinition(appDefinitionNote, accountViewModel) card.cover?.let { - AsyncImage( - model = it, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = - Modifier - .size(Size75dp) - .clip(QuoteBorder), - ) - } ?: run { NoteAuthorPicture(appDefinitionNote, Size75dp, accountViewModel = accountViewModel, nav = nav) } + Box(contentAlignment = BottomStart) { + MyAsyncImage( + imageUrl = it, + contentDescription = card.name, + contentScale = ContentScale.Crop, + mainImageModifier = Modifier, + loadedImageModifier = SimpleImage75Modifier, + accountViewModel = accountViewModel, + onLoadingBackground = { + appDefinitionNote.author?.let { author -> + BannerImage(author, SimpleImage75Modifier, accountViewModel) + } + }, + onError = { + appDefinitionNote.author?.let { author -> + BannerImage(author, SimpleImage75Modifier, accountViewModel) + } + }, + ) + } + } ?: run { + appDefinitionNote.author?.let { author -> + Box(contentAlignment = BottomStart) { + BannerImage(author, SimpleImage75Modifier, accountViewModel) + } + } + } Spacer(modifier = DoubleVertSpacer) @@ -592,16 +609,33 @@ fun FeedEmptyWithStatus( val card = observeAppDefinition(appDefinitionNote, accountViewModel) card.cover?.let { - AsyncImage( - model = it, - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = - Modifier - .size(Size75dp) - .clip(QuoteBorder), - ) - } ?: run { NoteAuthorPicture(appDefinitionNote, Size75dp, accountViewModel = accountViewModel, nav = nav) } + Box(contentAlignment = BottomStart) { + MyAsyncImage( + imageUrl = it, + contentDescription = card.name, + contentScale = ContentScale.Crop, + mainImageModifier = Modifier, + loadedImageModifier = SimpleImage75Modifier, + accountViewModel = accountViewModel, + onLoadingBackground = { + appDefinitionNote.author?.let { author -> + BannerImage(author, SimpleImage75Modifier, accountViewModel) + } + }, + onError = { + appDefinitionNote.author?.let { author -> + BannerImage(author, SimpleImage75Modifier, accountViewModel) + } + }, + ) + } + } ?: run { + appDefinitionNote.author?.let { author -> + Box(contentAlignment = BottomStart) { + BannerImage(author, SimpleImage75Modifier, accountViewModel) + } + } + } Spacer(modifier = DoubleVertSpacer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt index 2440a7a649..f37cfa9705 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt @@ -21,23 +21,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.dvms import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.ui.components.LoadNote +import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton -import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture +import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.Size34dp +import com.vitorpamplona.amethyst.ui.theme.SimpleImage35Modifier @Composable fun DvmTopBar( @@ -52,22 +48,34 @@ fun DvmTopBar( val card = observeAppDefinition(appDefinitionNote, accountViewModel) card.cover?.let { - AsyncImage( - model = it, - contentDescription = null, + MyAsyncImage( + imageUrl = it, + contentDescription = card.name, contentScale = ContentScale.Crop, - modifier = - Modifier - .size(Size34dp) - .clip(shape = CircleShape), + mainImageModifier = Modifier, + loadedImageModifier = SimpleImage35Modifier, + accountViewModel = accountViewModel, + onLoadingBackground = { + appDefinitionNote.author?.let { author -> + BannerImage(author, SimpleImage35Modifier, accountViewModel) + } + }, + onError = { + appDefinitionNote.author?.let { author -> + BannerImage(author, SimpleImage35Modifier, accountViewModel) + } + }, ) - } ?: run { NoteAuthorPicture(baseNote = appDefinitionNote, size = Size34dp, accountViewModel = accountViewModel) } + } ?: run { + appDefinitionNote.author?.let { author -> + BannerImage(author, SimpleImage35Modifier, accountViewModel) + } + } Spacer(modifier = DoubleHorzSpacer) Text( text = card.name, - fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 23752b92e7..1eaa1d6b38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -340,6 +340,9 @@ val FollowSetImageModifier = .clip(QuoteBorder) .aspectRatio(ratio = 21f / 9f) +val SimpleImage75Modifier = Modifier.size(Size75dp).clip(QuoteBorder) +val SimpleImage35Modifier = Modifier.size(Size34dp).clip(shape = CircleShape) + val SimpleImageBorder = Modifier.fillMaxSize().clip(QuoteBorder) val SimpleHeaderImage = Modifier.fillMaxWidth().heightIn(max = 200.dp) From 7a14d95973fe2fd5bcf08dbf4deba121a62cb34a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 14:34:09 -0400 Subject: [PATCH 073/185] Adds more relays to the DVM inbox. Sends the new EVENT only after the screen updates. --- .../src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index a673021416..89f42567a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1913,12 +1913,12 @@ class Account( dvmPublicKey .inboxRelays() .ifEmpty { - LocalCache.relayHints.hintsForKey(dvmPublicKey.pubkeyHex) + LocalCache.relayHints.hintsForKey(dvmPublicKey.pubkeyHex) + dvmPublicKey.relaysBeingUsed.keys }.toSet() - client.send(it, relayList) cache.justConsumeMyOwnEvent(it) onReady(it) + client.send(it, relayList) } } From f1d39b8517b34cdadecd005360e3f37eea0bac8b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 14:36:31 -0400 Subject: [PATCH 074/185] Logs the relays used for deletion events and checks if it hasn't sent the deleted event to each relay already --- .../amethyst/model/LocalCache.kt | 127 ++++++++++-------- 1 file changed, 69 insertions(+), 58 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index c177f69036..c1a2348bc3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -24,6 +24,7 @@ import android.util.Log import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag @@ -1255,72 +1256,74 @@ object LocalCache : ILocalCache { relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { - if (deletionIndex.add(event, wasVerified)) { - var deletedAtLeastOne = false + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) - event - .deleteEvents() - .mapNotNull { getNoteIfExists(it) } - .forEach { deleteNote -> - val deleteNoteEvent = deleteNote.event - if (deleteNoteEvent is AddressableEvent) { - val addressableNote = getAddressableNoteIfExists(deleteNoteEvent.addressTag()) - if (addressableNote?.author?.pubkeyHex == event.pubKey && (addressableNote.createdAt() ?: 0) <= event.createdAt) { - // Counts the replies - deleteNote(addressableNote) + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } - addressables.remove(addressableNote.address) + // Already processed this event. + if (note.event != null) return false - deletedAtLeastOne = true + if (wasVerified || justVerify(event)) { + note.loadEvent(event, author, emptyList()) + + if (deletionIndex.add(event, wasVerified)) { + event + .deleteEvents() + .mapNotNull { getNoteIfExists(it) } + .forEach { deleteNote -> + val deleteNoteEvent = deleteNote.event + if (deleteNoteEvent is AddressableEvent) { + val addressableNote = getAddressableNoteIfExists(deleteNoteEvent.addressTag()) + if (addressableNote?.author?.pubkeyHex == event.pubKey && (addressableNote.createdAt() ?: 0) <= event.createdAt) { + // Counts the replies + deleteNote(addressableNote) + + addressables.remove(addressableNote.address) + } + } + + // must be the same author + if (deleteNote.author?.pubkeyHex == event.pubKey) { + // reverts the add + deleteNote(deleteNote) } } - // must be the same author - if (deleteNote.author?.pubkeyHex == event.pubKey) { - // reverts the add - deleteNote(deleteNote) + val addressList = event.deleteAddressIds() + val addressSet = addressList.toSet() - deletedAtLeastOne = true + addressList + .mapNotNull { getAddressableNoteIfExists(it) } + .forEach { deleteNote -> + // must be the same author + if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) <= event.createdAt) { + // Counts the replies + deleteNote(deleteNote) + + addressables.remove(deleteNote.address) + } } - } - val addressList = event.deleteAddressIds() - val addressSet = addressList.toSet() - - addressList - .mapNotNull { getAddressableNoteIfExists(it) } - .forEach { deleteNote -> - // must be the same author - if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) <= event.createdAt) { - // Counts the replies - deleteNote(deleteNote) - - addressables.remove(deleteNote.address) - - deletedAtLeastOne = true - } - } - - notes.forEach { key, note -> - val noteEvent = note.event - if (noteEvent is AddressableEvent && noteEvent.addressTag() in addressSet) { - if (noteEvent.pubKey == event.pubKey && noteEvent.createdAt <= event.createdAt) { - deleteNote(note) - deletedAtLeastOne = true + notes.forEach { key, note -> + val noteEvent = note.event + if (noteEvent is AddressableEvent && noteEvent.addressTag() in addressSet) { + if (noteEvent.pubKey == event.pubKey && noteEvent.createdAt <= event.createdAt) { + deleteNote(note) + } } } } - if (deletedAtLeastOne) { - val note = Note(event.id) - note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList()) - refreshObservers(note) - } + refreshObservers(note) return true + } else { + return false } - - return false } @Suppress("DEPRECATION") @@ -2978,9 +2981,16 @@ object LocalCache : ILocalCache { if (deletionIndex.hasBeenDeleted(event)) { // update relay with deletion event from another. if (relay != null) { - deletionIndex.hasBeenDeletedBy(event)?.let { - Log.d("LocalCache", "Updating ${relay.url.url} with a Deletion Event ${it.toJson()} because of ${event.toJson()}") - relay.send(it) + deletionIndex.hasBeenDeletedBy(event)?.let { deletionEvent -> + getNoteIfExists(deletionEvent.id)?.let { note -> + if (!note.hasRelay(relay.url)) { + if (isDebug) { + Log.d("LocalCache", "Updating ${relay.url.url} with a Deletion Event ${event.id} ${deletionEvent.id} because of ${event.toJson()} with ${deletionEvent.toJson()}") + } + relay.send(deletionEvent) + note.addRelay(relay.url) + } + } } } return false @@ -2990,13 +3000,14 @@ object LocalCache : ILocalCache { // updates relay with a new event. getAddressableNoteIfExists(event.address())?.let { note -> note.event?.let { existingEvent -> - if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay.url)) { - Log.d("LocalCache", "Updating ${relay.url.url} with a new version of ${event.kind} ${event.id} to ${existingEvent.id}") - - // only send once. - note.addRelay(relay.url) + if (existingEvent.createdAt > event.createdAt && !note.hasRelay(relay.url) && !deletionIndex.hasBeenDeleted(event)) { + if (isDebug) { + Log.d("LocalCache", "Updating ${relay.url.url} with a new version of ${event.kind} ${event.id} to ${existingEvent.id}") + } relay.send(existingEvent) + // only send once. + note.addRelay(relay.url) } } } From 94a7d9cf1495870d58a4a20a59f517d1499f26f2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 14:52:27 -0400 Subject: [PATCH 075/185] Makes sure the list is not empty before building the filter --- .../FilterRepliesAndReactionsToNotes.kt | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt index f88122b5f4..a6c6d05949 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToNotes.kt @@ -81,38 +81,42 @@ fun filterRepliesAndReactionsToNotes( val since = since?.get(it.key)?.time val sortedList = it.value.sorted() val relay = it.key - listOf( - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = RepliesAndReactionsKinds, - tags = mapOf("e" to sortedList), - since = since, - // Max amount of "replies" to download on a specific event. - limit = 1000, - ), - ), - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = RepliesAndReactionsKinds2, - tags = mapOf("e" to sortedList), - since = since, - limit = 100, - ), - ), - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = listOf(TextNoteEvent.KIND), - tags = mapOf("q" to sortedList), - since = since, - limit = 1000, - ), - ), - ) + if (sortedList.isNotEmpty()) { + listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = RepliesAndReactionsKinds, + tags = mapOf("e" to sortedList), + since = since, + // Max amount of "replies" to download on a specific event. + limit = 1000, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = RepliesAndReactionsKinds2, + tags = mapOf("e" to sortedList), + since = since, + limit = 100, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = listOf(TextNoteEvent.KIND), + tags = mapOf("q" to sortedList), + since = since, + limit = 1000, + ), + ), + ) + } else { + emptyList() + } } } From a207d80d559d573e51d9ce5dba50a6d5c6ee9d95 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 15:51:57 -0400 Subject: [PATCH 076/185] Separating Reports from Metadata watching --- .../user/UserFinderFilterAssembler.kt | 2 + .../user/watchers/FilterReportsToKey.kt | 28 ++-- .../user/watchers/UserReportsSubAssembler.kt | 127 ++++++++++++++++++ .../user/watchers/UserWatcherSubAssembler.kt | 11 +- 4 files changed, 146 insertions(+), 22 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt index c736b6e45e..957107df0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserFinderFilterAssembler.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.loaders.UserLoaderSubAssembler +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserReportsSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers.UserWatcherSubAssembler import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient @@ -40,6 +41,7 @@ class UserFinderFilterAssembler( listOf( UserLoaderSubAssembler(client, ::allKeys), UserWatcherSubAssembler(client, ::allKeys), + UserReportsSubAssembler(client, ::allKeys), ) override fun invalidateFilters() = group.forEach { it.invalidateFilters() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt index bbb5b535f9..d21cce173b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/FilterReportsToKey.kt @@ -35,17 +35,21 @@ fun filterReportsToKeysFromTrusted( since: SincePerRelayMap?, ): List { if (targets.isEmpty() || trustedAccounts.isEmpty()) return emptyList() - val sortedTargets = targets.sorted() - return trustedAccounts.map { - RelayBasedFilter( - relay = it.key, - filter = - Filter( - kinds = ReportKindList, - authors = it.value.sorted(), - tags = mapOf("p" to sortedTargets), - since = since?.get(it.key)?.time, - ), - ) + val sortedTargets = mapOf("p" to targets.sorted()) + return trustedAccounts.mapNotNull { + if (it.value.isNotEmpty()) { + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = ReportKindList, + authors = it.value.sorted(), + tags = sortedTargets, + since = since?.get(it.key)?.time, + ), + ) + } else { + null + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt new file mode 100644 index 0000000000..acbde3be27 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.watchers + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState +import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.ammolite.relays.filters.MutableTime +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.mapOfSet +import kotlin.collections.flatten + +class UserReportsSubAssembler( + client: NostrClient, + allKeys: () -> Set, +) : SingleSubEoseManager(client, allKeys) { + var lastUsersOnFilter: Set = emptySet() + + /** + * This assembler saves the EOSE per user key. That EOSE includes their metadata, etc + * and reports, but only from trusted accounts (follows of all logged in users). + */ + var latestEOSEs: EOSEAccountFast = EOSEAccountFast(2000) + + override fun newEose( + relay: NormalizedRelayUrl, + time: Long, + ) { + lastUsersOnFilter.forEach { + latestEOSEs.newEose(it, relay, time) + } + super.newEose(relay, time) + } + + override fun updateFilter( + keys: List, + since: SincePerRelayMap?, + ): List? { + if (keys.isEmpty()) return null + + lastUsersOnFilter = keys.mapTo(mutableSetOf()) { it.user } + + if (lastUsersOnFilter.isEmpty()) return null + + val accounts = keys.mapTo(mutableSetOf()) { it.account } + + val trustedAccounts = + mapOfSet { + accounts.map { it.followsPerRelay.value }.forEach { + add(it) + } + } + + return groupByRelayPresence(lastUsersOnFilter, latestEOSEs, trustedAccounts.keys) + .map { group -> + val groupIds = group.map { it.pubkeyHex }.toSet() + if (groupIds.isNotEmpty()) { + val minEOSEs = findMinimumEOSEsForUsers(group, latestEOSEs) + filterReportsToKeysFromTrusted(groupIds, trustedAccounts, minEOSEs) + } else { + emptyList() + } + }.flatten() + } + + fun groupByRelayPresence( + users: Iterable, + eoseCache: EOSEAccountFast, + inRelays: Set, + ): Collection> = + users + .groupBy { + eoseCache + .since(it) + ?.keys + ?.intersect(inRelays) + ?.hashCode() + }.values + .map { + // important to keep in order otherwise the Relay thinks the filter has changed and we REQ again + it.sortedBy { it.pubkeyHex } + } + + fun findMinimumEOSEsForUsers( + users: List, + eoseCache: EOSEAccountFast, + ): SincePerRelayMap { + val minLatestEOSEs = mutableMapOf() + + users.forEach { + eoseCache.since(it)?.forEach { + val minEose = minLatestEOSEs[it.key] + if (minEose == null) { + minLatestEOSEs.put(it.key, it.value.copy()) + } else { + minEose.updateIfOlder(it.value.time) + } + } + } + + return minLatestEOSEs + } + + override fun distinct(key: UserFinderQueryState) = key.user +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt index 48efdbda3b..7102a33eb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.ammolite.relays.filters.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.utils.mapOfSet import kotlin.collections.flatten class UserWatcherSubAssembler( @@ -67,20 +66,12 @@ class UserWatcherSubAssembler( if (lastUsersOnFilter.isEmpty()) return null - val trustedAccounts = - mapOfSet { - keys.mapTo(mutableSetOf()) { it.account }.map { it.followsPerRelay.value }.forEach { - add(it) - } - } - return groupByRelayPresence(lastUsersOnFilter, latestEOSEs) .map { group -> val groupIds = group.map { it.pubkeyHex }.toSet() if (groupIds.isNotEmpty()) { val minEOSEs = findMinimumEOSEsForUsers(group, latestEOSEs) - filterUserMetadataForKey(groupIds, minEOSEs) + - filterReportsToKeysFromTrusted(groupIds, trustedAccounts, minEOSEs) + filterUserMetadataForKey(groupIds, minEOSEs) } else { emptyList() } From 191f1344db61e96acd25d3a14f316b6e9f373013 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 17:02:31 -0400 Subject: [PATCH 077/185] Starts diskcache together with video cache --- .../src/main/java/com/vitorpamplona/amethyst/Amethyst.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index fc3031dba1..bf885532f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -154,7 +154,10 @@ class Amethyst : Application() { } // initializes diskcache on an IO thread. - applicationIOScope.launch { videoCache } + applicationIOScope.launch { + diskCache + videoCache + } // registers to receive events pokeyReceiver.register(this) From 4eb348654f5f049ac3de56fb30b86dbf87ca7898 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 17:04:19 -0400 Subject: [PATCH 078/185] Providing a delete fix for empty drafts. --- .../vitorpamplona/amethyst/model/Account.kt | 20 +++++++++++++-- .../amethyst/model/LocalCache.kt | 6 +++++ .../metadata/FilterDraftsAndReportsFromKey.kt | 4 +-- .../loggedIn/PrecacheNewNotesProcessor.kt | 25 ++++++++++++++++--- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 89f42567a7..08f8ab8b4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -675,13 +675,28 @@ class Account( myRelayList.addAll(it.relays) } - client.send(deletionEvent, outboxRelays.flow.value + myRelayList) + client.send(deletionEvent, myRelayList) cache.justConsumeMyOwnEvent(deletionEvent) } } } } + fun delete( + event: Event, + additionalRelays: Set, + ) { + if (!isWriteable()) return + if (event.pubKey != signer.pubKey) return + + signer.sign( + DeletionEvent.build(listOf(event)), + ) { deletionEvent -> + client.send(deletionEvent, outboxRelays.flow.value + additionalRelays) + cache.justConsumeMyOwnEvent(deletionEvent) + } + } + suspend fun createHTTPAuthorization( url: String, method: String, @@ -1438,9 +1453,10 @@ class Account( noteEvent.createDeletedEvent(signer) { client.send(it, outboxRelays.flow.value + note.relays) cache.justConsumeMyOwnEvent(it) + + delete(it, note.relays.toSet()) } } - delete(note) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index c1a2348bc3..1856dca7f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2784,6 +2784,12 @@ object LocalCache : ILocalCache { if (consumeBaseReplaceable(event, relay, wasVerified)) { return true } + } else { + // passes to the AccountViewModel for further delete. + val note = Note(event.id) + note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList()) + relay?.let { note.addRelay(it) } + refreshObservers(note) } return false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt index 0416ae94c0..fbb5c5b9b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent -val DraftsAndReportsFromKeyKinds = +val ReportsAndBookmarksFromKeyKinds = listOf( DraftEvent.KIND, ReportEvent.KIND, @@ -47,7 +47,7 @@ fun filterDraftsAndReportsFromKey( relay = relay, filter = Filter( - kinds = DraftsAndReportsFromKeyKinds, + kinds = ReportsAndBookmarksFromKeyKinds, authors = listOf(pubkey), since = since, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/PrecacheNewNotesProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/PrecacheNewNotesProcessor.kt index 1418caa61b..6b56353260 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/PrecacheNewNotesProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/PrecacheNewNotesProcessor.kt @@ -68,9 +68,13 @@ class PrecacheNewNotesProcessor( is DraftEvent -> { // Avoid decrypting over and over again if the event already exist. - if (!event.isDeleted() && event.preCachedDraft(account.signer) == null && event.pubKey == account.signer.pubKey) { - event.cachedDraft(account.signer) { - cache.indexDraftAsRealEvent(event, it) + if (event.pubKey == account.signer.pubKey) { + if (!event.isDeleted()) { + if (event.preCachedDraft(account.signer) == null) { + event.cachedDraft(account.signer) { + cache.indexDraftAsRealEvent(event, it) + } + } } } } @@ -135,6 +139,21 @@ class PrecacheNewNotesProcessor( newNotes.forEach { consume(it) } + + val toDelete = + newNotes.mapNotNull { + val noteEvent = it.event + if (noteEvent is DraftEvent && noteEvent.isDeleted()) { + it + } else { + null + } + } + + if (toDelete.isNotEmpty()) { + Log.w("PrecacheNewNotesProcessor", "Deleting ${toDelete.size} draft notes that should have been deleted already") + account.delete(toDelete) + } } catch (e: Exception) { if (e is CancellationException) throw e Log.e("PrecacheNewNotesProcessor", "This shouldn't happen", e) From a706005cfdc68c538381ddda43e2b6690756c590 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 18:06:30 -0400 Subject: [PATCH 079/185] Fixes recent label reuse --- .../main/java/com/vitorpamplona/amethyst/LocalPreferences.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 414021289e..526d28f9e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -98,8 +98,8 @@ private object PrefKeys { const val LATEST_DM_RELAY_LIST = "latestDMRelayList" const val LATEST_NIP65_RELAY_LIST = "latestNIP65RelayList" const val LATEST_SEARCH_RELAY_LIST = "latestSearchRelayList" - const val LATEST_BLOCKED_RELAY_LIST = "latestSearchRelayList" - const val LATEST_TRUSTED_RELAY_LIST = "latestSearchRelayList" + const val LATEST_BLOCKED_RELAY_LIST = "latestBlockedRelayList" + const val LATEST_TRUSTED_RELAY_LIST = "latestTrustedRelayList" const val LATEST_MUTE_LIST = "latestMuteList" const val LATEST_PRIVATE_HOME_RELAY_LIST = "latestPrivateHomeRelayList" const val LATEST_APP_SPECIFIC_DATA = "latestAppSpecificData" From 5daaa6364de57bf00ab3d44c61ea5753d78c99c8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 18:07:01 -0400 Subject: [PATCH 080/185] Fixes issue with unable to parse text-based content in the event. --- .../definition/AppDefinitionEvent.kt | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt index 355fb3de89..ad1e90b00b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip89AppHandlers.definition import android.util.Log +import android.util.Log.e import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -55,9 +56,16 @@ class AppDefinitionEvent( cachedMetadata } else { try { - val newMetadata = AppMetadata.parse(content) - cachedMetadata = newMetadata - newMetadata + if (content.startsWith("{")) { + val newMetadata = AppMetadata.parse(content) + cachedMetadata = newMetadata + newMetadata + } else { + val newMetadata = AppMetadata() + newMetadata.name = content + cachedMetadata = newMetadata + newMetadata + } } catch (e: Exception) { e.printStackTrace() Log.w("AppDefinitionEvent", "Content Parse Error: ${toNostrUri()} ${e.localizedMessage}") From 62253d86010dddaab22f60344731b7718cb96a79 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 18:07:57 -0400 Subject: [PATCH 081/185] Finish the migration to separate channels --- .../vitorpamplona/amethyst/model/Account.kt | 9 +--- .../amethyst/model/LocalCache.kt | 19 +++++-- .../com/vitorpamplona/amethyst/model/Note.kt | 23 -------- .../loaders/FilterMissingAddressables.kt | 19 ++----- .../event/loaders/FilterMissingEvents.kt | 19 ++----- .../amethyst/ui/note/NoteCompose.kt | 42 ++++++++++----- .../amethyst/ui/note/types/Poll.kt | 3 +- .../amethyst/ui/note/types/Text.kt | 6 +-- .../PublicChatChannelHeader.kt | 20 ++++--- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 53 ++++++++++--------- .../loggedIn/threadview/ThreadFeedView.kt | 16 ++++-- 11 files changed, 102 insertions(+), 127 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 08f8ab8b4e..22c64adb5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -760,14 +760,7 @@ class Account( } } - val isInChannel = note.channelHex() - val channelRelays = - if (isInChannel != null) { - val channel = LocalCache.checkGetOrCreateChannel(isInChannel) - channel?.relays() ?: emptySet() - } else { - emptySet() - } + val channelRelays = LocalCache.getAnyChannel(note)?.relays() ?: emptySet() val replyRelays = note.replyTo?.flatMapTo(mutableSetOf()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1856dca7f0..4f13d4f56d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1326,6 +1326,19 @@ object LocalCache : ILocalCache { } } + fun getAnyChannel(note: Note): Channel? = note.event?.let { getAnyChannel(it) } + + fun getAnyChannel(noteEvent: Event): Channel? = + when (noteEvent) { + is ChannelCreateEvent -> getPublicChatChannelIfExists(noteEvent.id) + is ChannelMetadataEvent -> noteEvent.channelId()?.let { getPublicChatChannelIfExists(it) } + is ChannelMessageEvent -> noteEvent.channelId()?.let { getPublicChatChannelIfExists(it) } + is LiveActivitiesChatMessageEvent -> noteEvent.activityAddress()?.let { getLiveActivityChannelIfExists(it) } + is LiveActivitiesEvent -> getLiveActivityChannelIfExists(noteEvent.address()) + is EphemeralChatEvent -> noteEvent.roomId()?.let { getEphemeralChatChannelIfExists(it) } + else -> null + } + @Suppress("DEPRECATION") private fun deleteNote(deleteNote: Note) { val deletedEvent = deleteNote.event @@ -1349,11 +1362,7 @@ object LocalCache : ILocalCache { masterNote.removeReport(deleteNote) } - deleteNote.channelHex()?.let { getPublicChatChannelIfExists(it)?.removeNote(deleteNote) } - - (deletedEvent as? LiveActivitiesChatMessageEvent)?.activity()?.let { - getPublicChatChannelIfExists(it.toTag())?.removeNote(deleteNote) - } + getAnyChannel(deleteNote)?.removeNote(deleteNote) (deletedEvent as? TorrentCommentEvent)?.torrentIds()?.let { getNoteIfExists(it)?.removeReply(deleteNote) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 267b16318f..372381c549 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward -import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -51,8 +50,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip37Drafts.DraftEvent @@ -60,7 +57,6 @@ import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType @@ -214,25 +210,6 @@ open class Note( open fun idDisplayNote() = idNote().toShortDisplay() - fun channelHex(): HexKey? = - if ( - event is ChannelMessageEvent || - event is ChannelMetadataEvent || - event is ChannelCreateEvent || - event is LiveActivitiesChatMessageEvent || - event is LiveActivitiesEvent || - event is EphemeralChatEvent - ) { - (event as? ChannelMessageEvent)?.channelId() - ?: (event as? ChannelMetadataEvent)?.channelId() - ?: (event as? ChannelCreateEvent)?.id - ?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag() - ?: (event as? LiveActivitiesEvent)?.aTag()?.toTag() - ?: (event as? EphemeralChatEvent)?.roomId()?.toKey() - } else { - null - } - open fun address(): Address? = null open fun createdAt() = event?.createdAt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt index bcad63005e..d37c23dab7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt @@ -35,21 +35,12 @@ fun potentialRelaysToFindAddress(note: AddressableNote): Set set.addAll(LocalCache.relayHints.hintsForAddress(note.idHex)) - val isInChannel = note.channelHex() - if (isInChannel != null) { - LocalCache.checkGetOrCreateChannel(isInChannel)?.relays()?.forEach { - set.add(it) - } - } + LocalCache.getAnyChannel(note)?.relays()?.let { set.addAll(it) } note.replyTo?.map { parentNote -> set.addAll(parentNote.relays) - parentNote - .channelHex() - ?.let { LocalCache.checkGetOrCreateChannel(it) } - ?.relays() - ?.forEach { set.add(it) } + LocalCache.getAnyChannel(parentNote)?.relays()?.let { set.addAll(it) } parentNote.author?.inboxRelays()?.let { set.addAll(it) } } @@ -57,11 +48,7 @@ fun potentialRelaysToFindAddress(note: AddressableNote): Set note.replies.map { childNote -> set.addAll(childNote.relays) - childNote - .channelHex() - ?.let { LocalCache.checkGetOrCreateChannel(it) } - ?.relays() - ?.forEach { set.add(it) } + LocalCache.getAnyChannel(childNote)?.relays()?.let { set.addAll(it) } childNote.author?.outboxRelays()?.let { set.addAll(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt index d526f358bf..b8f6e1745f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingEvents.kt @@ -34,21 +34,12 @@ fun potentialRelaysToFindEvent(note: Note): Set { set.addAll(LocalCache.relayHints.hintsForEvent(note.idHex)) - val isInChannel = note.channelHex() - if (isInChannel != null) { - LocalCache.checkGetOrCreateChannel(isInChannel)?.relays()?.forEach { - set.add(it) - } - } + LocalCache.getAnyChannel(note)?.relays()?.let { set.addAll(it) } note.replyTo?.map { parentNote -> set.addAll(parentNote.relays) - parentNote - .channelHex() - ?.let { LocalCache.checkGetOrCreateChannel(it) } - ?.relays() - ?.forEach { set.add(it) } + LocalCache.getAnyChannel(parentNote)?.relays()?.let { set.addAll(it) } parentNote.author?.inboxRelays()?.let { set.addAll(it) } } @@ -56,11 +47,7 @@ fun potentialRelaysToFindEvent(note: Note): Set { note.replies.map { childNote -> set.addAll(childNote.relays) - childNote - .channelHex() - ?.let { LocalCache.checkGetOrCreateChannel(it) } - ?.relays() - ?.forEach { set.add(it) } + LocalCache.getAnyChannel(childNote)?.relays()?.let { set.addAll(it) } childNote.author?.outboxRelays()?.let { set.addAll(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 0a8dde8252..d70b05343b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -272,16 +272,24 @@ fun AcceptableNote( nav: INav, ) { if (isQuotedNote || isBoostedNote) { - when (baseNote.event) { - is ChannelCreateEvent, - is ChannelMetadataEvent, - -> + val noteEvent = baseNote.event + when (noteEvent) { + is ChannelCreateEvent -> RenderPublicChatChannelHeader( - channelNote = baseNote, + channelId = noteEvent.id, sendToChannel = true, accountViewModel = accountViewModel, nav = nav, ) + is ChannelMetadataEvent -> + noteEvent.channelId()?.let { + RenderPublicChatChannelHeader( + channelId = it, + sendToChannel = true, + accountViewModel = accountViewModel, + nav = nav, + ) + } is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let { RenderCommunity( @@ -311,16 +319,23 @@ fun AcceptableNote( } } } else { - when (baseNote.event) { - is ChannelCreateEvent, - is ChannelMetadataEvent, - -> + when (val noteEvent = baseNote.event) { + is ChannelCreateEvent -> RenderPublicChatChannelHeader( - channelNote = baseNote, + channelId = noteEvent.id, sendToChannel = true, accountViewModel = accountViewModel, nav = nav, ) + is ChannelMetadataEvent -> + noteEvent.channelId()?.let { + RenderPublicChatChannelHeader( + channelId = it, + sendToChannel = true, + accountViewModel = accountViewModel, + nav = nav, + ) + } is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let { RenderCommunity( @@ -1170,7 +1185,8 @@ private fun RenderAuthorImages( nav: INav, accountViewModel: AccountViewModel, ) { - if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) { + val noteEvent = baseNote.event + if (noteEvent is RepostEvent || noteEvent is GenericRepostEvent) { val baseRepost = baseNote.replyTo?.lastOrNull() if (baseRepost != null) { RepostNoteAuthorPicture(baseNote, baseRepost, accountViewModel, nav) @@ -1181,8 +1197,8 @@ private fun RenderAuthorImages( NoteAuthorPicture(baseNote, Size55dp, accountViewModel = accountViewModel, nav = nav) } - if (baseNote.event is ChannelMessageEvent) { - val baseChannelHex = remember(baseNote) { baseNote.channelHex() } + if (noteEvent is ChannelMessageEvent) { + val baseChannelHex = noteEvent.channelId() if (baseChannelHex != null) { LoadPublicChatChannel(baseChannelHex, accountViewModel) { channel -> ChannelNotePicture( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index 7039c77b65..4cb79ba44a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -32,6 +32,7 @@ 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.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer @@ -76,7 +77,7 @@ fun RenderPoll( val replyingTo = noteEvent.replyingToAddressOrEvent() if (replyingTo != null) { val newNote = accountViewModel.getNoteIfExists(replyingTo) - if (newNote != null && newNote.channelHex() == null && newNote.event?.kind != CommunityDefinitionEvent.KIND) { + if (newNote != null && LocalCache.getAnyChannel(newNote) == null && newNote.event?.kind != CommunityDefinitionEvent.KIND) { newNote } else { note.replyTo?.lastOrNull { it.event?.kind != CommunityDefinitionEvent.KIND } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 6f43bdb4d9..baef7d2914 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -33,6 +33,7 @@ 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.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.SensitivityWarning @@ -44,7 +45,6 @@ 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.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList @@ -66,7 +66,7 @@ fun RenderTextEvent( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = note.event as? Event ?: return + val noteEvent = note.event ?: return val showReply by remember(note) { @@ -82,7 +82,7 @@ fun RenderTextEvent( val replyingTo = noteEvent.replyingToAddressOrEvent() if (replyingTo != null) { val newNote = accountViewModel.getNoteIfExists(replyingTo) - if (newNote != null && newNote.channelHex() == null && newNote.event?.kind != CommunityDefinitionEvent.KIND) { + if (newNote != null && LocalCache.getAnyChannel(newNote) == null && newNote.event?.kind != CommunityDefinitionEvent.KIND) { newNote } else { note.replyTo?.lastOrNull { it.event?.kind != CommunityDefinitionEvent.KIND } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt index 7ee8a6c243..28b935b0e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -32,23 +31,22 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28P import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.innerPostModifier +import com.vitorpamplona.quartz.nip01Core.core.HexKey @Composable fun RenderPublicChatChannelHeader( - channelNote: Note, + channelId: HexKey, sendToChannel: Boolean, accountViewModel: AccountViewModel, nav: INav, ) { - channelNote.channelHex()?.let { - PublicChatChannelHeader( - channelHex = it, - sendToChannel = sendToChannel, - modifier = MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp), - accountViewModel = accountViewModel, - nav = nav, - ) - } + PublicChatChannelHeader( + channelHex = channelId, + sendToChannel = sendToChannel, + modifier = MaterialTheme.colorScheme.innerPostModifier.padding(Size10dp), + accountViewModel = accountViewModel, + nav = nav, + ) } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 87c05338d1..4b35b61baa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -102,7 +102,8 @@ class ChatroomListKnownFeedFilter( newRelevantPublicMessages.forEach { newNotePair -> var hasUpdated = false oldList.forEach { oldNote -> - if (newNotePair.key == oldNote.channelHex()) { + val channelId = (oldNote.event as? ChannelMessageEvent)?.channelId() + if (newNotePair.key == channelId) { hasUpdated = true if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) { myNewList = myNewList.replace(oldNote, newNotePair.value) @@ -117,7 +118,8 @@ class ChatroomListKnownFeedFilter( newRelevantEphemeralChats.forEach { newNotePair -> var hasUpdated = false oldList.forEach { oldNote -> - if (newNotePair.key.toKey() == oldNote.channelHex()) { + val noteEvent = (oldNote.event as? EphemeralChatEvent)?.roomId() + if (newNotePair.key == noteEvent) { hasUpdated = true if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) { myNewList = myNewList.replace(oldNote, newNotePair.value) @@ -171,17 +173,17 @@ class ChatroomListKnownFeedFilter( val followingChannels = account.publicChatList.flowSet.value val newRelevantPublicMessages = mutableMapOf() newItems - .filter { it.event is ChannelMessageEvent } .forEach { newNote -> - newNote.channelHex()?.let { channelHex -> - if (channelHex in followingChannels && account.isAcceptable(newNote)) { - val lastNote = newRelevantPublicMessages.get(channelHex) + val channelId = (newNote.event as? ChannelMessageEvent)?.channelId() + if (channelId != null) { + if (channelId in followingChannels && account.isAcceptable(newNote)) { + val lastNote = newRelevantPublicMessages.get(channelId) if (lastNote != null) { if ((newNote.createdAt() ?: 0) > (lastNote.createdAt() ?: 0)) { - newRelevantPublicMessages.put(channelHex, newNote) + newRelevantPublicMessages.put(channelId, newNote) } } else { - newRelevantPublicMessages.put(channelHex, newNote) + newRelevantPublicMessages.put(channelId, newNote) } } } @@ -198,7 +200,6 @@ class ChatroomListKnownFeedFilter( newItems .forEach { newNote -> val noteEvent = newNote.event as? EphemeralChatEvent - if (noteEvent != null) { val room = noteEvent.roomId() if (room != null && room in followingEphemeralChats && account.isAcceptable(newNote)) { @@ -225,27 +226,27 @@ class ChatroomListKnownFeedFilter( val newRelevantPrivateMessages = mutableMapOf() newItems - .filter { it.event is ChatroomKeyable } .forEach { newNote -> val roomKey = (newNote.event as? ChatroomKeyable)?.chatroomKey(me.pubkeyHex) - val room = account.userProfile().privateChatrooms[roomKey] - - if (roomKey != null && room != null) { - if ( - ( - newNote.author?.pubkeyHex == me.pubkeyHex || - room.senderIntersects(followingKeySet) || - me.hasSentMessagesTo(roomKey) - ) && - !account.isAllHidden(roomKey.users) - ) { - val lastNote = newRelevantPrivateMessages.get(roomKey) - if (lastNote != null) { - if ((newNote.createdAt() ?: 0) > (lastNote.createdAt() ?: 0)) { + if (roomKey != null) { + val room = account.userProfile().privateChatrooms[roomKey] + if (room != null) { + if ( + ( + newNote.author?.pubkeyHex == me.pubkeyHex || + room.senderIntersects(followingKeySet) || + me.hasSentMessagesTo(roomKey) + ) && + !account.isAllHidden(roomKey.users) + ) { + val lastNote = newRelevantPrivateMessages.get(roomKey) + if (lastNote != null) { + if ((newNote.createdAt() ?: 0) > (lastNote.createdAt() ?: 0)) { + newRelevantPrivateMessages.put(roomKey, newNote) + } + } else { newRelevantPrivateMessages.put(roomKey, newNote) } - } else { - newRelevantPrivateMessages.put(roomKey, newNote) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index e97ca99502..107de49eef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -521,16 +521,22 @@ private fun FullBleedNoteCompose( .padding(horizontal = 12.dp), ) { Column { - if ( - (noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && - baseNote.channelHex() != null - ) { + if (noteEvent is ChannelCreateEvent) { PublicChatChannelHeader( - channelHex = baseNote.channelHex()!!, + channelHex = noteEvent.id, sendToChannel = true, accountViewModel = accountViewModel, nav = nav, ) + } else if (noteEvent is ChannelMetadataEvent) { + noteEvent.channelId()?.let { + PublicChatChannelHeader( + channelHex = it, + sendToChannel = true, + accountViewModel = accountViewModel, + nav = nav, + ) + } } else if (noteEvent is VideoEvent) { VideoDisplay(baseNote, makeItShort = false, canPreview = true, backgroundColor = backgroundColor, ContentScale.FillWidth, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is PictureEvent) { From f3e08bd297673fdcf711564ad4db3881e4c77877 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 18:08:09 -0400 Subject: [PATCH 082/185] No need for this OkHttp log anymore --- .../amethyst/service/okhttp/OkHttpClientFactory.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index 769d2372ee..a6aabc68ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -20,12 +20,6 @@ */ package com.vitorpamplona.amethyst.service.okhttp -import android.util.Log -import com.vitorpamplona.amethyst.isDebug -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import okhttp3.Dispatcher import okhttp3.OkHttpClient import java.net.InetSocketAddress @@ -51,6 +45,8 @@ class OkHttpClientFactory( maxRequests = 512 } + /* + DEBUG OK HTTP connections here. init { if (isDebug) { GlobalScope.launch(Dispatchers.IO) { @@ -61,6 +57,7 @@ class OkHttpClientFactory( } } } + */ private val rootClient = OkHttpClient From dc61c5a8e46f27db8d13b175798e0d14cd07153c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 18:08:32 -0400 Subject: [PATCH 083/185] Keeps cache of the content inside Public chat forums --- .../admin/ChannelCreateEvent.kt | 32 +++++++++++++++---- .../admin/ChannelMetadataEvent.kt | 31 ++++++++++++++---- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index 5953c078a2..b6c1e6f956 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -22,11 +22,13 @@ package com.vitorpamplona.quartz.nip28PublicChat.admin import android.R.attr.data import android.util.Log +import android.util.Log.e import androidx.compose.runtime.Immutable import com.fasterxml.jackson.core.JsonParseException import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -46,17 +48,33 @@ class ChannelCreateEvent( sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider { + @Transient + var cache: ChannelDataNorm? = null + override fun eventHints() = channelInfo().relays?.map { EventIdHint(id, it) } ?: emptyList() override fun linkedEventIds() = listOf(id) - fun channelInfo(): ChannelDataNorm = - try { - ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() - } catch (e: JsonParseException) { - Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) - ChannelDataNorm() - } + fun isEncrypted() = tags.any { it.has(1) && it[0] == "encrypted" && it[1] == "true" } + + fun channelInfo(): ChannelDataNorm { + cache?.let { return it } + + val newInfo = + try { + if (isEncrypted()) { + ChannelDataNorm() + } else { + ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() + } + } catch (e: JsonParseException) { + Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) + ChannelDataNorm() + } + + cache = newInfo + return newInfo + } companion object { const val KIND = 40 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt index 6446edcef2..526214f20b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelMetadataEvent.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.Immutable import com.fasterxml.jackson.core.JsonParseException import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint @@ -48,6 +49,9 @@ class ChannelMetadataEvent( sig: HexKey, ) : BasePublicChatEvent(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider { + @Transient + var cache: ChannelDataNorm? = null + override fun eventHints() = channelInfo().relays?.mapNotNull { relay -> channelId()?.let { EventIdHint(it, relay) } @@ -55,13 +59,26 @@ class ChannelMetadataEvent( override fun linkedEventIds() = channelId()?.let { listOf(it) } ?: emptyList() - fun channelInfo(): ChannelDataNorm = - try { - ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() - } catch (e: JsonParseException) { - Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) - ChannelDataNorm() - } + fun isEncrypted() = tags.any { it.has(1) && it[0] == "encrypted" && it[1] == "true" } + + fun channelInfo(): ChannelDataNorm { + cache?.let { return it } + + val newInfo = + try { + if (isEncrypted()) { + ChannelDataNorm() + } else { + ChannelData.parse(content)?.normalize() ?: ChannelDataNorm() + } + } catch (e: JsonParseException) { + Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) + ChannelDataNorm() + } + + cache = newInfo + return newInfo + } companion object { const val KIND = 41 From e3acf4c250cc6fd3420b2ea8d3cee7231d3df77c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 18:22:38 -0400 Subject: [PATCH 084/185] Fixes some of the live stream flags when the event doesn't update. --- .../amethyst/ui/note/types/LiveActivity.kt | 2 +- .../StreamingStatusFlags.kt | 15 ++++++--- .../DiscoverLiveFeedFilter.kt | 2 +- .../nip53LiveActivities/LiveActivityCard.kt | 2 +- .../streaming/LiveActivitiesEvent.kt | 31 ++++++++----------- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index f6a5f2f93b..4944db6f0e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -139,7 +139,7 @@ fun RenderLiveActivityEventInner( val subject = remember(eventUpdates) { noteEvent.title() } val content = remember(eventUpdates) { noteEvent.summary() } val participants = remember(eventUpdates) { noteEvent.participants() } - val status = remember(eventUpdates) { noteEvent.statusEnum() } + val status = remember(eventUpdates) { noteEvent.status() } val starts = remember(eventUpdates) { noteEvent.starts() } Row( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt index 671d0d89fb..81d36e517e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/StreamingStatusFlags.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.SmallBorder import com.vitorpamplona.amethyst.ui.theme.liveStreamTag +import com.vitorpamplona.quartz.utils.TimeUtils import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Date @@ -92,11 +93,15 @@ fun OfflineFlag() { fun ScheduledFlag(starts: Long?) { val startsIn = starts?.let { - SimpleDateFormat - .getDateTimeInstance( - DateFormat.SHORT, - DateFormat.SHORT, - ).format(Date(starts * 1000)) + if (it > TimeUtils.now()) { + SimpleDateFormat + .getDateTimeInstance( + DateFormat.SHORT, + DateFormat.SHORT, + ).format(Date(starts * 1000)) + } else { + null + } } Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt index a67cb2f4ba..2f711d7038 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt @@ -94,7 +94,7 @@ open class DiscoverLiveFeedFilter( return collection .sortedWith( compareBy( - { convertStatusToOrder((it.event as? LiveActivitiesEvent)?.statusEnum()) }, + { convertStatusToOrder((it.event as? LiveActivitiesEvent)?.status()) }, { participantCounts[it] }, { allParticipants[it] }, { (it.event as? LiveActivitiesEvent)?.starts() ?: it.createdAt() }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 897cea356b..b31d46c232 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -105,7 +105,7 @@ fun RenderLiveActivityThumb( subject = noteEvent?.title()?.ifBlank { null }, content = noteEvent?.summary(), participants = noteEvent?.participants()?.toImmutableList() ?: persistentListOf(), - status = noteEvent?.statusEnum(), + status = noteEvent?.status(), starts = noteEvent?.starts(), ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt index 6b6c0fad80..140a1dcd22 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt @@ -29,14 +29,8 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseKey import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag @@ -88,11 +82,9 @@ class LiveActivitiesEvent( fun ends() = tags.firstNotNullOfOrNull(EndsTag::parse) - fun status() = checkStatus(tags.firstNotNullOfOrNull(StatusTag::parse)) + fun status() = checkStatus(tags.firstNotNullOfOrNull(StatusTag::parseEnum)) - fun statusEnum() = checkStatusEnum(tags.firstNotNullOfOrNull(StatusTag::parseEnum)) - - fun isLive() = statusEnum() == StatusTag.STATUS.LIVE + fun isLive() = status() == StatusTag.STATUS.LIVE fun currentParticipants() = tags.firstNotNullOfOrNull(CurrentParticipantsTag::parse) @@ -110,16 +102,19 @@ class LiveActivitiesEvent( fun hosts() = tags.mapNotNull(ParticipantTag::parseHost) - fun checkStatus(eventStatus: String?): String? = - if (eventStatus == StatusTag.STATUS.LIVE.code && createdAt < TimeUtils.eightHoursAgo()) { - StatusTag.STATUS.ENDED.code - } else { - eventStatus - } - - fun checkStatusEnum(eventStatus: StatusTag.STATUS?): StatusTag.STATUS? = + fun checkStatus(eventStatus: StatusTag.STATUS?): StatusTag.STATUS? = if (eventStatus == StatusTag.STATUS.LIVE && createdAt < TimeUtils.eightHoursAgo()) { StatusTag.STATUS.ENDED + } else if (eventStatus == StatusTag.STATUS.PLANNED) { + val starts = starts() + val ends = ends() + if (starts != null && starts < TimeUtils.oneHourAgo()) { + StatusTag.STATUS.ENDED + } else if (ends != null && ends < TimeUtils.oneHourAgo()) { + StatusTag.STATUS.ENDED + } else { + eventStatus + } } else { eventStatus } From a94c8673d5c3a4be72f1280b3adf2631ca39e656 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 10 Jul 2025 19:42:18 -0400 Subject: [PATCH 085/185] Fixing Ephemeral Chat interface --- .../vitorpamplona/amethyst/model/Account.kt | 34 +--- .../amethyst/model/LocalCache.kt | 155 ++++-------------- .../loggedIn/home/dal/HomeLiveFilter.kt | 4 +- .../experimental/ephemChat/chat/RoomId.kt | 11 +- .../ephemChat/chat/tags/RelayTag.kt | 2 +- .../client/single/basic/BasicRelayClient.kt | 1 - 6 files changed, 46 insertions(+), 161 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 22c64adb5b..a45ccfef2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -70,7 +70,6 @@ import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent -import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent @@ -145,9 +144,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.emojis import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent @@ -163,8 +159,6 @@ import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent @@ -815,33 +809,7 @@ class Account( ?: cache.relayHints.hintsForKey(pubkey).toSet() } - private fun computeRelaysForChannels(event: Event): Set { - val isInChannel = - if ( - event is ChannelMessageEvent || - event is ChannelMetadataEvent || - event is ChannelCreateEvent || - event is LiveActivitiesChatMessageEvent || - event is LiveActivitiesEvent || - event is EphemeralChatEvent - ) { - (event as? ChannelMessageEvent)?.channelId() - ?: (event as? ChannelMetadataEvent)?.channelId() - ?: (event as? ChannelCreateEvent)?.id - ?: (event as? LiveActivitiesChatMessageEvent)?.activity()?.toTag() - ?: (event as? LiveActivitiesEvent)?.aTag()?.toTag() - ?: (event as? EphemeralChatEvent)?.roomId()?.toKey() - } else { - null - } - - return if (isInChannel != null) { - val channel = LocalCache.checkGetOrCreateChannel(isInChannel) - channel?.relays() ?: emptySet() - } else { - emptySet() - } - } + private fun computeRelaysForChannels(event: Event): Set = LocalCache.getAnyChannel(event)?.relays() ?: emptySet() fun computeRelayListToBroadcast(event: Event): Set { if (event is MetadataEvent || event is AdvertisedRelayListEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 4f13d4f56d..a50c55a562 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -61,7 +61,6 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -416,33 +415,16 @@ object LocalCache : ILocalCache { EphemeralChatChannel(key) } - fun checkGetOrCreateChannel(key: String): Channel? { - checkNotInMainThread() - - if (key.contains("@")) { - val idParts = key.split("@") - val relay = RelayUrlNormalizer.normalizeOrNull(idParts[1]) - - if (relay == null) { - return null - } else { - getOrCreateEphemeralChannel(RoomId(idParts[0], relay)) - } - } - + fun checkGetOrCreatePublicChatChannel(key: String): PublicChatChannel? { if (isValidHex(key)) { return getOrCreatePublicChatChannel(key) } - - val address = Address.parse(key) - if (address != null) { - return getOrCreateLiveChannel(address) - } return null } private fun isValidHex(key: String): Boolean { if (key.isBlank()) return false + if (key.length != 64) return false if (key.contains(":")) return false return Hex.isHex(key) @@ -1622,12 +1604,12 @@ object LocalCache : ILocalCache { if (channelId.isNullOrBlank()) return false // new event - val oldChannel = checkGetOrCreateChannel(channelId) ?: return false + val oldChannel = checkGetOrCreatePublicChatChannel(channelId) ?: return false val author = getOrCreateUser(event.pubKey) val isVerified = if (event.createdAt > oldChannel.updatedMetadataAt) { - if (oldChannel is PublicChatChannel && (wasVerified || justVerify(event))) { + if (wasVerified || justVerify(event)) { oldChannel.updateChannelInfo(author, event) true } else { @@ -1653,44 +1635,18 @@ object LocalCache : ILocalCache { relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { - val channelId = event.channelId() + val channelId = event.channelId() ?: return false - if (channelId.isNullOrBlank()) return false - - val channel = checkGetOrCreateChannel(channelId) ?: return false + val channel = checkGetOrCreatePublicChatChannel(channelId) + if (channel == null) { + Log.w("LocalCache", "Unable to create public chat channel for event ${event.toJson()}") + return false + } val note = getOrCreateNote(event.id) channel.addNote(note, relay) - 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 false - - if (antiSpam.isSpam(event, relay)) { - return false - } - - if (wasVerified || justVerify(event)) { - val replyTo = computeReplyTo(event) - - note.loadEvent(event, author, replyTo) - - // Log.d("CM", "New Chat Note (${note.author?.toBestDisplayName()} ${note.event?.content} - // ${formattedDateTime(event.createdAt)}") - - // Counts the replies - replyTo.forEach { it.addReply(note) } - - refreshObservers(note) - } - - return true + return consumeRegularEvent(event, relay, wasVerified) } fun consume( @@ -1698,46 +1654,15 @@ object LocalCache : ILocalCache { relay: NormalizedRelayUrl?, wasVerified: Boolean, ): Boolean { - val roomId = event.roomId() - if (roomId == null) return false - - val channelId = roomId - val channel = getOrCreateEphemeralChannel(channelId) ?: return false + val roomId = event.roomId() ?: return false val note = getOrCreateNote(event.id) + val channel = getOrCreateEphemeralChannel(roomId) channel.addNote(note, relay) - 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 false - - if (antiSpam.isSpam(event, relay)) { - return false - } - - if (wasVerified || justVerify(event)) { - note.loadEvent(event, author, emptyList()) - - refreshObservers(note) - - return true - } - - return false + return consumeRegularEvent(event, relay, wasVerified) } - fun consume( - event: CommentEvent, - relay: NormalizedRelayUrl?, - wasVerified: Boolean, - ) = consumeRegularEvent(event, relay, wasVerified) - fun consume( event: LiveActivitiesChatMessageEvent, relay: NormalizedRelayUrl?, @@ -1750,36 +1675,15 @@ object LocalCache : ILocalCache { val note = getOrCreateNote(event.id) channel.addNote(note, relay) - 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 false - - if (antiSpam.isSpam(event, relay)) { - return false - } - - if (wasVerified || justVerify(event)) { - val replyTo = computeReplyTo(event) - - note.loadEvent(event, author, replyTo) - - // Counts the replies - replyTo.forEach { it.addReply(note) } - - refreshObservers(note) - - return true - } - - return false + return consumeRegularEvent(event, relay, wasVerified) } + fun consume( + event: CommentEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeRegularEvent(event, relay, wasVerified) + @Suppress("UNUSED_PARAMETER") fun consume( event: ChannelHideMessageEvent, @@ -2859,18 +2763,18 @@ object LocalCache : ILocalCache { } } is EphemeralChatEvent -> { - draft.roomId()?.toKey()?.let { - checkGetOrCreateChannel(it)?.addNote(note, null) + draft.roomId()?.let { + getOrCreateEphemeralChannel(it).addNote(note, null) } } is ChannelMessageEvent -> { draft.channelId()?.let { channelId -> - checkGetOrCreateChannel(channelId)?.addNote(note, null) + checkGetOrCreatePublicChatChannel(channelId)?.addNote(note, null) } } is LiveActivitiesChatMessageEvent -> { draft.activityAddress()?.let { channelId -> - checkGetOrCreateChannel(channelId.toValue())?.addNote(note, null) + getOrCreateLiveChannel(channelId).addNote(note, null) } } is TextNoteEvent -> { @@ -2937,12 +2841,17 @@ object LocalCache : ILocalCache { } is ChannelMessageEvent -> { draft.channelId()?.let { channelId -> - checkGetOrCreateChannel(channelId)?.removeNote(draftWrap) + getPublicChatChannelIfExists(channelId)?.removeNote(draftWrap) } } is EphemeralChatEvent -> { draft.roomId()?.let { - getOrCreateEphemeralChannel(it).removeNote(draftWrap) + getEphemeralChatChannelIfExists(it)?.removeNote(draftWrap) + } + } + is LiveActivitiesChatMessageEvent -> { + draft.activityAddress()?.let { channelId -> + getLiveActivityChannelIfExists(channelId)?.removeNote(draftWrap) } } is TextNoteEvent -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index ae1a0f7a72..7da2420821 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -124,7 +124,7 @@ class HomeLiveFilter( } fun sort(collection: Set): List { - val topFilter = account.liveDiscoveryFollowLists.value + val topFilter = account.liveHomeFollowLists.value val topFilterAuthors = when (topFilter) { is AuthorsByOutboxTopNavFilter -> topFilter.authors @@ -137,7 +137,7 @@ class HomeLiveFilter( val followingKeySet = topFilterAuthors ?: account.kind3FollowList.flow.value.authors val followCounts = - collection.associate { it to followsThatParticipateOn(it, followingKeySet) } + collection.associateWith { followsThatParticipateOn(it, followingKeySet) } return collection.sortedWith( compareByDescending { followCounts[it] } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt index d249be3516..884b19def1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt @@ -26,8 +26,17 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl data class RoomId( val id: String, val relayUrl: NormalizedRelayUrl, -) { +) : Comparable { fun toKey() = "$id@$relayUrl" fun toDisplayKey() = id + "@" + relayUrl.displayUrl() + + override fun compareTo(other: RoomId): Int { + val result = id.compareTo(other.id) + return if (result == 0) { + relayUrl.url.compareTo(other.relayUrl.url) + } else { + result + } + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt index 4f596e31ec..3399d98fc9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/tags/RelayTag.kt @@ -39,7 +39,7 @@ class RelayTag { ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - return RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null + return RelayUrlNormalizer.normalizeOrNull(tag[1]) } @JvmStatic diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index fcfac8e720..b4faad56b8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -149,7 +149,6 @@ open class BasicRelayClient( override fun onMessage(text: String) { // Log.d(logTag, "Receiving: $text") - stats.addBytesReceived(text.bytesUsedInMemory()) try { From 2ecfeb6e7fd064872ff23eb666b6aec89f709e6c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 11 Jul 2025 08:51:02 -0400 Subject: [PATCH 086/185] Moves subscription management from Ammolite to Quatrz --- .gitignore | 1 + .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../relayClient/CacheClientConnector.kt | 4 ++-- .../authCommand/model/AuthCoordinator.kt | 2 +- .../eoseManagers/BaseEoseManager.kt | 2 +- .../eoseManagers/PerUniqueIdEoseManager.kt | 2 +- .../PerUserAndFollowListEoseManager.kt | 2 +- .../eoseManagers/PerUserEoseManager.kt | 2 +- .../notifyCommand/model/NotifyCoordinator.kt | 2 +- .../metadata/AccountMetadataEoseManager.kt | 2 +- .../AccountNotificationsEoseManager.kt | 2 +- .../AccountGiftWrapsEoseManager.kt | 2 +- .../amethyst/service/relays/EOSE.kt | 9 ------- .../DMsFromUserFilterSubAssembler.kt | 2 +- .../FollowingEphemeralChatSubAssembler.kt | 2 +- .../FollowingPublicChatSubAssembler.kt | 2 +- .../DiscoveryFollowsDiscoverySubAssembler1.kt | 2 +- .../DiscoveryFollowsDiscoverySubAssembler2.kt | 2 +- .../DiscoveryFollowsDiscoverySubAssembler3.kt | 2 +- .../HomeOutboxEventsEoseManager.kt | 2 +- .../VideoOutboxEventsFilterSubAssembler.kt | 2 +- gradle/libs.versions.toml | 2 +- .../nip01Core/relay/client/NostrClient.kt | 24 ++++++++++++++++++- .../EventCollector.kt | 2 +- .../NostrClientSendAndWaitExt.kt} | 2 +- .../NostrClientSingleDownloadExt.kt | 2 +- .../RelayAuthenticator.kt | 2 +- .../RelayInsertConfirmationCollector.kt | 2 +- .../RelayLogger.kt | 2 +- .../RelayNotifier.kt | 2 +- .../nip01Core/relay/client/pool/RelayPool.kt | 20 +++++++++++++++- .../client/single/basic/BasicRelayClient.kt | 18 ++++++++++++++ .../client/subscriptions}/Subscription.kt | 6 +---- .../subscriptions}/SubscriptionController.kt | 20 +++++++++++++--- .../subscriptions}/SubscriptionStats.kt | 2 +- .../quartz/nip01Core/relay/filters/Filter.kt | 20 ++++++++++++++++ 36 files changed, 128 insertions(+), 48 deletions(-) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/{acessories => accessories}/EventCollector.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/{acessories/NostrClientSendAndWait.kt => accessories/NostrClientSendAndWaitExt.kt} (98%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/{acessories => accessories}/NostrClientSingleDownloadExt.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/{acessories => accessories}/RelayAuthenticator.kt (96%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/{acessories => accessories}/RelayInsertConfirmationCollector.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/{acessories => accessories}/RelayLogger.kt (97%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/{acessories => accessories}/RelayNotifier.kt (96%) rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources => quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions}/Subscription.kt (91%) rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources => quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions}/SubscriptionController.kt (81%) rename {ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources => quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions}/SubscriptionStats.kt (96%) diff --git a/.gitignore b/.gitignore index e5303fd75d..f527be1420 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ /.idea/studiobot.xml /.idea/other.xml /.idea/runConfigurations.xml +/.idea/ChatHistory_schema_v2.xml .DS_Store /build /captures diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index a45ccfef2b..d51d045b14 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -101,7 +101,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient -import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.downloadFirstEvent +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirstEvent import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt index 42b1d85786..f03417aceb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.service.relayClient import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient -import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.EventCollector -import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.RelayInsertConfirmationCollector +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.EventCollector +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayInsertConfirmationCollector import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class CacheClientConnector( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt index 078a2cce93..6954c39417 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt @@ -24,7 +24,7 @@ import android.util.Log import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient -import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.RelayAuthenticator +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayAuthenticator class ScreenAuthAccount( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt index ac84ed46ea..ea0c7db732 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/BaseEoseManager.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import android.util.Log import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.ammolite.relays.BundledUpdate -import com.vitorpamplona.ammolite.relays.datasources.SubscriptionController import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.SubscriptionController import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt index 606086a77b..e8a9292c15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -22,10 +22,10 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.service.relays.EOSEByKey import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlin.collections.distinctBy diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index 6593d6a8ac..16fe9ae1d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -23,10 +23,10 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountKey import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlin.collections.distinctBy diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index e6e139c7be..65e17aa7b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -23,10 +23,10 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.EOSEAccountFast import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlin.collections.distinctBy diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt index 90d3927374..135b9cc747 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/model/NotifyCoordinator.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient -import com.vitorpamplona.quartz.nip01Core.relay.client.acessories.RelayNotifier +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayNotifier class NotifyCoordinator( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt index 044a23d759..aacd90b3b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt @@ -24,9 +24,9 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt index efa2107a91..3556bedc83 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt @@ -24,9 +24,9 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt index 4601b97ca7..048e9b1e2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt @@ -24,9 +24,9 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index 1e24dc69e8..7c56420caf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.service.relays import androidx.collection.LruCache -import com.vitorpamplona.amethyst.model.LocalCache.users import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.ammolite.relays.filters.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -56,10 +55,6 @@ class EOSERelayList { ) = addOrUpdate(relay, time) } -class EOSEFollowList( - cacheSize: Int = 200, -) : EOSEByKey(cacheSize) - open class EOSEByKey( cacheSize: Int = 200, ) { @@ -89,10 +84,6 @@ open class EOSEByKey( ) = addOrUpdate(listCode, relayUrl, time) } -class EOSEAccount( - cacheSize: Int = 20, -) : EOSEAccountKey(cacheSize) - open class EOSEAccountKey( cacheSize: Int = 20, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt index 844b1f67d7..8c7df6be70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt index 7723ed123b..e4014d47f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt index fdbfef14dc..e17a7a4716 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt @@ -23,9 +23,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt index 122863ba5a..ef35137a63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt @@ -26,9 +26,9 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.makeLongFormFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.makeContentDVMsFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.makeClassifiedsFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt index a8839c5e81..e3c560c9fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt @@ -25,9 +25,9 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFol import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.makeFollowSetsFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.makeLiveActivitiesFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt index 2660a51aa0..5b945fd5e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt @@ -25,9 +25,9 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFol import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.makePublicChatsFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.makeCommunitiesFilter -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt index 755b551348..9d91716445 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt @@ -37,9 +37,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.f import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsByCommunity -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt index 774aebe750..26bb84f559 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt @@ -39,9 +39,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassembl import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip65Follows.filterPictureAndVideoByFollows import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities.filterPictureAndVideoByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities.filterPictureAndVideoByCommunity -import com.vitorpamplona.ammolite.relays.datasources.Subscription import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c227c8ff15..6f182838a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] accompanistAdaptive = "0.37.3" activityCompose = "1.10.1" -agp = "8.11.0" +agp = "8.11.1" android-compileSdk = "36" android-minSdk = "26" android-targetSdk = "36" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index c494a37d6c..7371deaae2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -46,7 +46,29 @@ import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.stateIn /** - * The Nostr Client manages a relay pool, keeps active subscriptions and manages re-sending of events. + * The NostrClient manages Nostr relay operations, subscriptions, and event delivery. It maintains: + * - A RelayPool for managing connections to a collection of Nostr relays + * - Active subscriptions tracking through PoolSubscriptionRepository + * - An event outbox for managing unsent events and retry logic + * - Automatic relay pool reconciliation based on subscription and event needs + * + * Core responsibilities include: + * - Initializing and managing relay connections using WebSocket builders + * - Coordinating subscription state across multiple relays + * - Handling event and filter resending when relays disconnect and reconnect + * - Aggregating relay status from the pool's state flow + * - Maintaining listeners for propagating relay events and state changes + * + * Features: + * - Reactive updating of relay sets based on subscription and outbox activity + * - Relayer reconnection strategy that processes pending requests + * - Filter comparison logic to detect significant subscription changes and avoid redundant requests + * - Integration with RelayStats for tracking relay performance + * - Thread-safe reconnection methods using coroutine flows + * + * The class combines flows from active subscriptions and the event outbox to ensure relays + * are connected to all necessary endpoints. It also listens to relay state changes to update + * subscriptions and message retries when connections are re-established. */ class NostrClient( private val websocketBuilder: WebsocketBuilder, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/EventCollector.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/EventCollector.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt index 42d63f53a5..4acf6c0835 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/EventCollector.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/EventCollector.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relay.client.acessories +package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSendAndWait.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSendAndWait.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt index 351041d9eb..dc80f0c898 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSendAndWait.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSendAndWaitExt.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relay.client.acessories +package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt index dbee706e9d..62869bfbb5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/NostrClientSingleDownloadExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientSingleDownloadExt.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relay.client.acessories +package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayAuthenticator.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayAuthenticator.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt index 9e60606e97..d80ce69796 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayAuthenticator.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relay.client.acessories +package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayInsertConfirmationCollector.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayInsertConfirmationCollector.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt index 6757dfa763..b23654bd75 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayInsertConfirmationCollector.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayInsertConfirmationCollector.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relay.client.acessories +package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayLogger.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayLogger.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt index d1d51a3b70..40bf64eb76 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayLogger.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relay.client.acessories +package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayNotifier.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt similarity index 96% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayNotifier.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt index 3d3a33fb65..289d5b95f3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/acessories/RelayNotifier.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayNotifier.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.relay.client.acessories +package com.vitorpamplona.quartz.nip01Core.relay.client.accessories import android.util.Log import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt index e179548407..d8af62a1b4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -41,7 +41,25 @@ val UnsupportedRelayCreation: (url: NormalizedRelayUrl) -> IRelayClient = { } /** - * RelayPool manages the connection to multiple Relays and lets consumers deal with simple events. + * RelayPool manages a collection of Nostr relays, abstracting individual connections and providing + * unified methods for sending events, managing subscriptions, and tracking relay states. + * + * Key features: + * - Maintains a cache of relays using LargeCache for efficient relay management + * - Propagates event notifications to a shared listener across all relays + * - Provides state tracking through RelayPoolStatus (connected/available relays) + * - Supports relay lifecycle operations like connect/disconnect/reconnect + * - Maintains an immutable createNewRelay function for custom relay creation + * + * Listens to relay events and: + * 1. Forwards callbacks to parent listener + * 2. Updates statusFlow when relay connectivity or events change + * 3. Automatically reconnects relays that need reconnection + * + * Common use cases: + * - Sending events to multiple relays simultaneously (send method) + * - Managing subscriptions across the relay pool (sendRequest/closed methods) + * - Maintaining optimal relay connections (updatePool/addRelay/removeRelay methods) */ class RelayPool( val listener: IRelayClientListener = EmptyClientListener, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index b4faad56b8..9a77aa6074 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -52,6 +52,24 @@ import com.vitorpamplona.quartz.utils.bytesUsedInMemory import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.cancellation.CancellationException +/** + * A base implementation for a relay client that establishes and manages a WebSocket connection to a Nostr relay. + * This class provides fundamental connection handling, message parsing, reconnection logic, and event dispatching. + * + * @property url The relay's normalized URL. + * @property socketBuilder Provides the WebSocket instance for connection. + * @property listener Interface to notify the application of relay events and errors. + * @property stats Tracks operational statistics of the relay connection. + * @property defaultOnConnect Callback executed after a successful connection, allowing subclasses to add initialization logic. + * + * Reconnection Strategy: + * - Uses exponential backoff to retry connections, starting with [DELAY_TO_RECONNECT_IN_MSECS] (500ms). + * - Doubles the delay between reconnection attempts in case of failure. + * + * Message Handling: + * - Processes relay messages (e.g., `EVENT`, `EOSE`, `OK`, `AUTH`) and delegates to appropriate callbacks. + * - Dispatches received events, notices, and subscription closures via the [listener]. + */ open class BasicRelayClient( override val url: NormalizedRelayUrl, val socketBuilder: WebsocketBuilder, diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/Subscription.kt similarity index 91% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/Subscription.kt index 364613cdbe..8ed3767696 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/Subscription.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/Subscription.kt @@ -18,12 +18,11 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.ammolite.relays.datasources +package com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlin.contracts.ExperimentalContracts data class Subscription( val id: String = newSubId(), @@ -41,9 +40,6 @@ data class Subscription( fun filters() = filters - @OptIn(ExperimentalContracts::class) - fun isActive() = filters != null - fun callEose( time: Long, relay: NormalizedRelayUrl, diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt similarity index 81% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt index b1a6f91695..dac7e7f2d0 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionController.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionController.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.ammolite.relays.datasources +package com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient @@ -29,8 +29,22 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.LargeCache /** - * Semantically groups Nostr filters and subscriptions in data source objects that - * maintain the desired active filter with the relay. + * Manages Nostr subscriptions using a [NostrClient], allowing subscriptions to be created, modified, + * and synchronized with relay filters. Subscriptions are stored in a cache and processed through + * [updateRelays] to update relay filters dynamically. Also tracks event statistics and EOSE (End of + * Stored Events) events, and provides utility methods to interact with subscriptions like dismissal. + * + * Key responsibilities: + * 1. Maintain a cache of active [Subscription] instances. + * 2. Handle client events (onEvent, onEOSE) using [clientListener]. + * 3. Synchronize relay filters via [updateRelays] when subscriptions change. + * 4. Provide methods to create, dismiss, and inspect subscriptions. + * + * Usage: + * - Use [requestNewSubscription] to create subscriptions. + * - Modify filters on [Subscription] at will and call [updateRelays] to apply changes. + * - Update filters based on EOSE callbacks on each subscription + * - Dismiss subscriptions with [dismissSubscription]. */ class SubscriptionController( val client: NostrClient, diff --git a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionStats.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionStats.kt similarity index 96% rename from ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionStats.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionStats.kt index 03e3330507..87d01b85eb 100644 --- a/ammolite/src/main/java/com/vitorpamplona/ammolite/relays/datasources/SubscriptionStats.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/subscriptions/SubscriptionStats.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.ammolite.relays.datasources +package com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions import android.util.Log import com.vitorpamplona.quartz.utils.LargeCache diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index 06bea99c58..dcd0990501 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -24,6 +24,23 @@ import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +/** + * A filter for Nostr events used in relay subscriptions. Supports various criteria + * to match events based on IDs, authors, kinds, tags, time ranges, search terms, and limits. + * + * Parameters: + * - ids: Optional list of event IDs to match (must be 64 characters). + * - authors: Optional list of author public keys (must be 64 characters). + * - kinds: Optional list of event kinds to include. + * - tags: Optional map of tag names to values arrays (common tags like 'p', 'e', 'a' are validated). + * - since: Optional timestamp for filtering events with publication time ≥ this value. + * - until: Optional timestamp for filtering events with publication time ≤ this value. + * - limit: Optional maximum number of events to request. + * - search: Optional string to search within event content. + * + * This class performs validation on construction to ensure all string-based identifiers + * follow Nostr requirements (64-char hex, onion addresses) and logs errors for invalid inputs. + */ class Filter( val ids: List? = null, val authors: List? = null, @@ -49,6 +66,9 @@ class Filter( search: String? = this.search, ) = Filter(ids, authors, kinds, tags, since, until, limit, search) + /** + * Returns true if this filter contains any non-null and non-empty criteria. + */ fun isFilledFilter() = (ids != null && ids.isNotEmpty()) || (authors != null && authors.isNotEmpty()) || From e37d6ebd8b97f218c141e54194ad82af21bfca65 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 11 Jul 2025 09:22:27 -0400 Subject: [PATCH 087/185] Moves media server from Dialog to Screen with full route. --- .../nipB7Blossom/BlossomServerListState.kt | 5 +- .../mediaServers/AllMediaServersLIstView.kt | 79 +++------ .../mediaServers/AllMediaServersScreen.kt | 145 ++++++++++++++++ .../amethyst/ui/navigation/AppNavigation.kt | 2 + .../amethyst/ui/navigation/DrawerContent.kt | 8 +- .../amethyst/ui/navigation/Routes.kt | 3 + .../loggedIn/relays/AllRelayListScreen.kt | 162 ++++++++++-------- .../relays/common/BasicRelaySetupInfoModel.kt | 5 +- .../relays/dm/AddDMRelayListDialog.kt | 6 +- .../relays/nip65/Nip65RelayListViewModel.kt | 5 +- .../search/AddSearchRelayListDialog.kt | 53 +++--- 11 files changed, 307 insertions(+), 166 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt index 69c910da80..927a730c78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent import com.vitorpamplona.quartz.utils.tryAndWait @@ -49,14 +48,14 @@ class BlossomServerListState( ) { fun getBlossomServersAddress() = BlossomServersEvent.createAddress(signer.pubKey) - fun getBlossomServersNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlossomServersAddress()) + fun getBlossomServersNote(): AddressableNote = cache.getOrCreateAddressableNote(getBlossomServersAddress()) fun getBlossomServersListFlow(): StateFlow = getBlossomServersNote().flow().metadata.stateFlow fun getBlossomServersList(): BlossomServersEvent? = getBlossomServersNote().event as? BlossomServersEvent fun normalizeServers(note: Note): List { - val event = note.event as? FileServersEvent + val event = note.event as? BlossomServersEvent return event?.servers() ?: emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt index 0beb600869..f89bb83ab2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt @@ -45,59 +45,30 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategoryWithButton import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.grayText -@Composable -fun MediaServersListView( - onClose: () -> Unit, - accountViewModel: AccountViewModel, - nav: INav, -) { - val nip96ServersViewModel: NIP96ServersViewModel = viewModel() - val blossomServersViewModel: BlossomServersViewModel = viewModel() - - LaunchedEffect(key1 = Unit) { - nip96ServersViewModel.load(accountViewModel.account) - blossomServersViewModel.load(accountViewModel.account) - } - - Dialog( - onDismissRequest = onClose, - properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false), - ) { - SetDialogToEdgeToEdge() - DialogContent(nip96ServersViewModel, blossomServersViewModel, onClose) - } -} - @OptIn(ExperimentalMaterial3Api::class) @Composable -fun DialogContent( +fun MediaServersListView( nip96ServersViewModel: NIP96ServersViewModel, blossomServersViewModel: BlossomServersViewModel, onClose: () -> Unit, @@ -119,6 +90,7 @@ fun DialogContent( }, navigationIcon = { CloseButton( + modifier = HalfHorzPadding, onPress = { nip96ServersViewModel.refresh() blossomServersViewModel.refresh() @@ -128,6 +100,7 @@ fun DialogContent( }, actions = { SaveButton( + modifier = HalfHorzPadding, onPost = { nip96ServersViewModel.saveFileServers() blossomServersViewModel.saveFileServers() @@ -182,32 +155,11 @@ fun AllMediaBody( horizontalAlignment = Alignment.CenterHorizontally, contentPadding = FeedPadding, ) { - item { - SettingsCategory( - stringRes(R.string.media_servers_nip96_section), - stringRes(R.string.media_servers_nip96_explainer), - SettingsCategoryFirstModifier, - ) - } - - renderMediaServerList( - mediaServersState = nip96ServersState, - keyType = "nip96", - editLabel = R.string.add_a_nip96_server, - emptyLabel = R.string.no_nip96_server_message, - onAddServer = { server -> - nip96ServersViewModel.addServer(server) - }, - onDeleteServer = { - nip96ServersViewModel.removeServer(serverUrl = it) - }, - ) - item { SettingsCategory( stringRes(R.string.media_servers_blossom_section), stringRes(R.string.media_servers_blossom_explainer), - SettingsCategorySpacingModifier, + SettingsCategoryFirstModifier, ) } @@ -224,6 +176,27 @@ fun AllMediaBody( }, ) + item { + SettingsCategory( + stringRes(R.string.media_servers_nip96_section), + stringRes(R.string.media_servers_nip96_explainer), + SettingsCategorySpacingModifier, + ) + } + + renderMediaServerList( + mediaServersState = nip96ServersState, + keyType = "nip96", + editLabel = R.string.add_a_nip96_server, + emptyLabel = R.string.no_nip96_server_message, + onAddServer = { server -> + nip96ServersViewModel.addServer(server) + }, + onDeleteServer = { + nip96ServersViewModel.removeServer(serverUrl = it) + }, + ) + DEFAULT_MEDIA_SERVERS.let { item { SettingsCategoryWithButton( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt new file mode 100644 index 0000000000..17be2ff5b8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.mediaServers + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton +import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding +import com.vitorpamplona.amethyst.ui.theme.grayText + +@Composable +fun AllMediaServersScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + val nip96ServersViewModel: NIP96ServersViewModel = viewModel() + val blossomServersViewModel: BlossomServersViewModel = viewModel() + + LaunchedEffect(key1 = Unit) { + nip96ServersViewModel.load(accountViewModel.account) + blossomServersViewModel.load(accountViewModel.account) + } + + MediaServersScaffold(nip96ServersViewModel, blossomServersViewModel) { + nav.popBack() + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MediaServersScaffold( + nip96ServersViewModel: NIP96ServersViewModel, + blossomServersViewModel: BlossomServersViewModel, + onClose: () -> Unit, +) { + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceAround, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringRes(id = R.string.media_servers), + style = MaterialTheme.typography.titleLarge, + ) + } + }, + navigationIcon = { + CloseButton( + modifier = HalfHorzPadding, + onPress = { + nip96ServersViewModel.refresh() + blossomServersViewModel.refresh() + onClose() + }, + ) + }, + actions = { + SaveButton( + modifier = HalfHorzPadding, + isActive = true, + onPost = { + nip96ServersViewModel.saveFileServers() + blossomServersViewModel.saveFileServers() + onClose() + }, + ) + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) + }, + ) { padding -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding( + start = 16.dp, + top = padding.calculateTopPadding(), + end = 16.dp, + bottom = padding.calculateBottomPadding(), + ).consumeWindowInsets(padding) + .imePadding(), + verticalArrangement = Arrangement.spacedBy(10.dp, alignment = Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + stringRes(id = R.string.set_preferred_media_servers), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.grayText, + ) + + AllMediaBody(nip96ServersViewModel, blossomServersViewModel) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 863975fecd..2970449099 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -48,6 +48,7 @@ import androidx.navigation.compose.composable import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.compose.DisplayNotifyMessages import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataScreen +import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.components.toasts.DisplayErrorMessages import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen @@ -131,6 +132,7 @@ fun AppNavigation( composableFromEnd { UserSettingsScreen(accountViewModel, nav) } composableFromBottomArgs { NIP47SetupScreen(accountViewModel, nav, it.nip47) } composableFromEndArgs { AllRelayListScreen(accountViewModel, nav) } + composableFromEndArgs { AllMediaServersScreen(accountViewModel, nav) } composableFromEndArgs { DvmContentDiscoveryScreen(it.id, accountViewModel, nav) } composableFromEndArgs { ProfileScreen(it.id, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 06ba28a751..37026ed367 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -96,7 +96,6 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowerCount import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.ui.actions.mediaServers.MediaServersListView import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.note.LoadStatuses @@ -422,8 +421,6 @@ fun ListContent( accountViewModel: AccountViewModel, nav: INav, ) { - var editMediaServers by remember { mutableStateOf(false) } - var backupDialogOpen by remember { mutableStateOf(false) } Column(modifier) { @@ -465,7 +462,7 @@ fun ListContent( tint = MaterialTheme.colorScheme.onBackground, onClick = { nav.closeDrawer() - editMediaServers = true + nav.nav(Route.EditMediaServers) }, ) @@ -524,9 +521,6 @@ fun ListContent( ) } - if (editMediaServers) { - MediaServersListView({ editMediaServers = false }, accountViewModel = accountViewModel, nav = nav) - } if (backupDialogOpen) { AccountBackupDialog(accountViewModel, onClose = { backupDialogOpen = false }) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index bbabd85617..4c7a06bb8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -55,6 +55,8 @@ sealed class Route { @Serializable object EditRelays : Route() + @Serializable object EditMediaServers : Route() + @Serializable data class Nip47NWCSetup( val nip47: String? = null, ) : Route() @@ -221,6 +223,7 @@ fun getRouteWithArguments(navController: NavHostController): Route? { dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() + dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() dest.hasRoute() -> entry.toRoute() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index d23aa6479d..4afe17001a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -20,10 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -40,7 +38,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -74,11 +71,10 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.TrustedRelay import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.renderTrustedItems import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.MinHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.grayText @Composable @@ -86,102 +82,116 @@ fun AllRelayListScreen( accountViewModel: AccountViewModel, nav: INav, ) { - MappedAllRelayListView(accountViewModel, nav) + val dmViewModel: DMRelayListViewModel = viewModel() + val nip65ViewModel: Nip65RelayListViewModel = viewModel() + val privateOutboxViewModel: PrivateOutboxRelayListViewModel = viewModel() + val searchViewModel: SearchRelayListViewModel = viewModel() + val blockedViewModel: BlockedRelayListViewModel = viewModel() + val trustedViewModel: TrustedRelayListViewModel = viewModel() + val localViewModel: LocalRelayListViewModel = viewModel() + val connectedViewModel: ConnectedRelayListViewModel = viewModel() + + dmViewModel.init(accountViewModel.account) + nip65ViewModel.init(accountViewModel.account) + searchViewModel.init(accountViewModel.account) + localViewModel.init(accountViewModel.account) + privateOutboxViewModel.init(accountViewModel.account) + connectedViewModel.init(accountViewModel.account) + blockedViewModel.init(accountViewModel.account) + trustedViewModel.init(accountViewModel.account) + + LaunchedEffect(accountViewModel.account) { + dmViewModel.load() + nip65ViewModel.load() + searchViewModel.load() + localViewModel.load() + privateOutboxViewModel.load() + connectedViewModel.load() + blockedViewModel.load() + trustedViewModel.load() + } + + MappedAllRelayListView( + dmViewModel, + nip65ViewModel, + searchViewModel, + localViewModel, + privateOutboxViewModel, + connectedViewModel, + blockedViewModel, + trustedViewModel, + accountViewModel, + nav, + ) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun MappedAllRelayListView( + dmViewModel: DMRelayListViewModel, + nip65ViewModel: Nip65RelayListViewModel, + searchViewModel: SearchRelayListViewModel, + localViewModel: LocalRelayListViewModel, + privateOutboxViewModel: PrivateOutboxRelayListViewModel, + connectedViewModel: ConnectedRelayListViewModel, + blockedViewModel: BlockedRelayListViewModel, + trustedViewModel: TrustedRelayListViewModel, accountViewModel: AccountViewModel, newNav: INav, ) { - val dmViewModel: DMRelayListViewModel = viewModel() val dmFeedState by dmViewModel.relays.collectAsStateWithLifecycle() - - val nip65ViewModel: Nip65RelayListViewModel = viewModel() val homeFeedState by nip65ViewModel.homeRelays.collectAsStateWithLifecycle() val notifFeedState by nip65ViewModel.notificationRelays.collectAsStateWithLifecycle() - - val privateOutboxViewModel: PrivateOutboxRelayListViewModel = viewModel() val privateOutboxFeedState by privateOutboxViewModel.relays.collectAsStateWithLifecycle() - - val searchViewModel: SearchRelayListViewModel = viewModel() val searchFeedState by searchViewModel.relays.collectAsStateWithLifecycle() - - val blockedViewModel: BlockedRelayListViewModel = viewModel() val blockedFeedState by blockedViewModel.relays.collectAsStateWithLifecycle() - - val trustedViewModel: TrustedRelayListViewModel = viewModel() val trustedFeedState by trustedViewModel.relays.collectAsStateWithLifecycle() - - val localViewModel: LocalRelayListViewModel = viewModel() val localFeedState by localViewModel.relays.collectAsStateWithLifecycle() - - val connectedViewModel: ConnectedRelayListViewModel = viewModel() val connectedRelays by connectedViewModel.relays.collectAsStateWithLifecycle() - LaunchedEffect(Unit) { - dmViewModel.load(accountViewModel.account) - nip65ViewModel.load(accountViewModel.account) - searchViewModel.load(accountViewModel.account) - localViewModel.load(accountViewModel.account) - privateOutboxViewModel.load(accountViewModel.account) - connectedViewModel.load(accountViewModel.account) - blockedViewModel.load(accountViewModel.account) - trustedViewModel.load(accountViewModel.account) - } - Scaffold( topBar = { TopAppBar( title = { - Row( + Text( + text = stringRes(R.string.relay_settings), modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = MinHorzSpacer) - - Text( - text = stringRes(R.string.relay_settings), - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - - SaveButton( - onPost = { - dmViewModel.create() - nip65ViewModel.create() - searchViewModel.create() - localViewModel.create() - privateOutboxViewModel.create() - trustedViewModel.create() - blockedViewModel.create() - newNav.popBack() - }, - true, - ) - } + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) }, navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - dmViewModel.clear() - nip65ViewModel.clear() - searchViewModel.clear() - localViewModel.clear() - privateOutboxViewModel.clear() - trustedViewModel.clear() - blockedViewModel.clear() - newNav.popBack() - }, - ) - } + CloseButton( + modifier = HalfHorzPadding, + onPress = { + dmViewModel.clear() + nip65ViewModel.clear() + searchViewModel.clear() + localViewModel.clear() + privateOutboxViewModel.clear() + trustedViewModel.clear() + blockedViewModel.clear() + newNav.popBack() + }, + ) + }, + actions = { + SaveButton( + modifier = HalfHorzPadding, + isActive = true, + onPost = { + dmViewModel.create() + nip65ViewModel.create() + searchViewModel.create() + localViewModel.create() + privateOutboxViewModel.create() + trustedViewModel.create() + blockedViewModel.create() + newNav.popBack() + }, + ) }, colors = TopAppBarDefaults.topAppBarColors( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index 5ae2115966..37151d9c75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -41,8 +41,11 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { var hasModified = false - fun load(account: Account) { + fun init(account: Account) { this.account = account + } + + fun load() { clear() loadRelayDocuments() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt index 0915c4d974..74240d809f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt @@ -64,7 +64,11 @@ fun AddDMRelayListDialog( ) { val postViewModel: DMRelayListViewModel = viewModel() - LaunchedEffect(Unit) { postViewModel.load(accountViewModel.account) } + postViewModel.init(accountViewModel.account) + + LaunchedEffect(accountViewModel.account) { + postViewModel.load() + } Dialog( onDismissRequest = onClose, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index fee19cddec..2b5d12620b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -49,8 +49,11 @@ class Nip65RelayListViewModel : ViewModel() { var hasModified = false - fun load(account: Account) { + fun init(account: Account) { this.account = account + } + + fun load() { clear() loadRelayDocuments() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt index 2648cc4f74..a5a0cea80e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.search import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -36,8 +35,9 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInf import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayListViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.imageModifier @@ -66,7 +66,11 @@ fun AddSearchRelayListDialog( ) { val postViewModel: SearchRelayListViewModel = viewModel() - LaunchedEffect(Unit) { postViewModel.load(accountViewModel.account) } + postViewModel.init(accountViewModel.account) + + LaunchedEffect(accountViewModel.account) { + postViewModel.load() + } Dialog( onDismissRequest = onClose, @@ -76,34 +80,35 @@ fun AddSearchRelayListDialog( Scaffold( topBar = { TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = StdHorzSpacer) - - Text(stringRes(R.string.search_relays_title)) - - SaveButton( - onPost = { - postViewModel.create() - onClose() - }, - true, - ) - } - }, navigationIcon = { - Spacer(modifier = StdHorzSpacer) CloseButton( + modifier = HalfHorzPadding, onPress = { postViewModel.clear() onClose() }, ) }, + title = { + Text( + text = stringRes(R.string.search_relays_title), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleLarge, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + }, + actions = { + SaveButton( + modifier = HalfHorzPadding, + isActive = true, + onPost = { + postViewModel.create() + onClose() + }, + ) + }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, From 15efd671ecd28573f5041ce11c23a107593b6f72 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 11 Jul 2025 12:54:08 -0400 Subject: [PATCH 088/185] Reorganizes navigation and unifies topbar designs --- .../components/TranslatableRichTextViewer.kt | 2 +- .../amethyst/model/AccountSettings.kt | 17 +++ .../amethyst/model/HashtagIcon.kt | 2 +- .../compose/DisplayNotifyMessages.kt | 2 +- .../compose/NotifyRequestDialog.kt | 4 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 4 +- .../amethyst/ui/actions/EditPostView.kt | 56 ++------ .../amethyst/ui/actions/NewMediaView.kt | 86 ++++++------ .../ui/actions/NewUserMetadataScreen.kt | 61 ++------- .../mediaServers/AllMediaServersLIstView.kt | 87 ------------ .../mediaServers/AllMediaServersScreen.kt | 59 ++------- .../amethyst/ui/components/ClickableRoute.kt | 6 +- .../ui/components/ExpandableRichTextViewer.kt | 2 +- .../amethyst/ui/components/RichTextViewer.kt | 8 +- .../markdown/MarkdownMediaRenderer.kt | 2 +- .../markdown/RenderContentAsMarkdown.kt | 4 +- .../components/toasts/DisplayErrorMessages.kt | 2 +- .../components/toasts/multiline/ErrorList.kt | 6 +- .../multiline/MultiUserErrorMessageDialog.kt | 4 +- .../amethyst/ui/feeds/FeedContentStateView.kt | 2 +- .../amethyst/ui/feeds/FeedLoaded.kt | 2 +- .../amethyst/ui/navigation/AppNavigation.kt | 42 +----- .../amethyst/ui/navigation/HeightDecreaser.kt | 76 ----------- ...BuilderEffects.kt => NavigationEffects.kt} | 16 +++ .../{ => bottombars}/AppBottomBar.kt | 78 +---------- .../navigation/bottombars/BottomBarRoutes.kt | 48 +++++++ .../ui/navigation/bottombars/KeyboardState.kt | 71 ++++++++++ .../{ => drawer}/AccountSwitchBottomSheet.kt | 2 +- .../navigation/{ => drawer}/DrawerContent.kt | 5 +- .../amethyst/ui/navigation/navs/EmptyNav.kt | 52 ++++++++ .../{BottomBarRoute.kt => navs/INav.kt} | 40 ++++-- .../ui/navigation/{INav.kt => navs/Nav.kt} | 124 +----------------- .../ui/navigation/navs/ObservableNav.kt | 81 ++++++++++++ .../RememberNavs.kt} | 38 +++--- .../ui/navigation/{ => routes}/RouteMaker.kt | 2 +- .../ui/navigation/{ => routes}/Routes.kt | 23 +++- .../ui/navigation/topbars/ActionTopBar.kt | 124 ++++++++++++++++++ .../{ => topbars}/AmethystClickableIcon.kt | 2 +- .../{ => topbars}/FeedFilterSpinner.kt | 2 +- .../ui/navigation/topbars/ShorterTopAppBar.kt | 57 ++++++++ .../TopBarExtensibleWithBackButton.kt | 6 +- .../{ => topbars}/TopBarWithBackButton.kt | 22 ++-- .../UserDrawerSearchTopBar.kt} | 57 +------- .../amethyst/ui/note/BadgeCompose.kt | 4 +- .../amethyst/ui/note/BlankNote.kt | 4 +- .../amethyst/ui/note/BlockReportChecker.kt | 2 +- .../vitorpamplona/amethyst/ui/note/Gallery.kt | 4 +- .../amethyst/ui/note/MessageSetCompose.kt | 4 +- .../amethyst/ui/note/MultiSetCompose.kt | 6 +- .../ui/note/NIP05VerificationDisplay.kt | 4 +- .../amethyst/ui/note/NoteCompose.kt | 4 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 6 +- .../amethyst/ui/note/PollNote.kt | 6 +- .../amethyst/ui/note/ReactionsRow.kt | 6 +- .../amethyst/ui/note/RelayListBox.kt | 2 +- .../amethyst/ui/note/RelayListRow.kt | 5 +- .../amethyst/ui/note/ReplyInformation.kt | 4 +- .../ui/note/UpdateReactionTypeDialog.kt | 38 ++---- .../amethyst/ui/note/UserCompose.kt | 4 +- .../amethyst/ui/note/UserProfilePicture.kt | 4 +- .../amethyst/ui/note/ZapNoteCompose.kt | 4 +- .../amethyst/ui/note/ZapUserSetCompose.kt | 4 +- .../note/creators/previews/DisplayPreviews.kt | 2 +- .../ui/note/creators/previews/PreviewUrl.kt | 2 +- .../creators/zapsplits/DisplayZapSplits.kt | 2 +- .../note/elements/AddInboxRelayForDMCard.kt | 4 +- .../ui/note/elements/DisplayCommunity.kt | 4 +- .../ui/note/elements/DisplayHashtags.kt | 4 +- .../ui/note/elements/DisplayLocation.kt | 4 +- .../ui/note/elements/DisplayReward.kt | 4 +- .../note/elements/DisplayUncitedHashtags.kt | 4 +- .../amethyst/ui/note/elements/DropDownMenu.kt | 4 +- .../amethyst/ui/note/elements/ForkInfo.kt | 4 +- .../ui/note/elements/ZapTheDevsCard.kt | 8 +- .../note/nip22Comments/DisplayExternalId.kt | 2 +- .../nip22Comments/DisplayGeoHashExternalId.kt | 4 +- .../nip22Comments/DisplayHashtagExternalId.kt | 4 +- .../nip22Comments/GenericCommentPostScreen.kt | 61 +++------ .../amethyst/ui/note/types/AppDefinition.kt | 2 +- .../amethyst/ui/note/types/AudioTrack.kt | 4 +- .../amethyst/ui/note/types/Badge.kt | 2 +- .../amethyst/ui/note/types/ChannelMessage.kt | 2 +- .../amethyst/ui/note/types/ChatMessage.kt | 4 +- .../ui/note/types/ChatMessageEncryptedFile.kt | 4 +- .../amethyst/ui/note/types/Classifieds.kt | 2 +- .../amethyst/ui/note/types/CommunityHeader.kt | 4 +- .../amethyst/ui/note/types/FollowList.kt | 2 +- .../amethyst/ui/note/types/Git.kt | 2 +- .../amethyst/ui/note/types/Highlight.kt | 4 +- .../ui/note/types/InteractiveStory.kt | 2 +- .../amethyst/ui/note/types/LiveActivity.kt | 6 +- .../ui/note/types/LiveActivityChatMessage.kt | 2 +- .../amethyst/ui/note/types/LongForm.kt | 2 +- .../amethyst/ui/note/types/MedicalData.kt | 2 +- .../types/NIP90ContentDiscoveryResponse.kt | 2 +- .../amethyst/ui/note/types/NIP90Status.kt | 2 +- .../amethyst/ui/note/types/PeopleList.kt | 2 +- .../amethyst/ui/note/types/PictureDisplay.kt | 2 +- .../amethyst/ui/note/types/PinList.kt | 2 +- .../amethyst/ui/note/types/Poll.kt | 2 +- .../amethyst/ui/note/types/PrivateMessage.kt | 4 +- .../amethyst/ui/note/types/Reaction.kt | 2 +- .../amethyst/ui/note/types/RelayList.kt | 4 +- .../ui/note/types/RenderPostApproval.kt | 2 +- .../amethyst/ui/note/types/Report.kt | 2 +- .../amethyst/ui/note/types/Text.kt | 2 +- .../ui/note/types/TextModification.kt | 4 +- .../amethyst/ui/note/types/Torrent.kt | 4 +- .../amethyst/ui/note/types/TorrentComment.kt | 6 +- .../amethyst/ui/note/types/Video.kt | 2 +- .../amethyst/ui/note/types/Wiki.kt | 2 +- .../amethyst/ui/screen/AccountState.kt | 2 +- .../ui/screen/AccountStateViewModel.kt | 2 +- .../amethyst/ui/screen/FeedView.kt | 2 +- .../amethyst/ui/screen/FollowListState.kt | 2 +- .../amethyst/ui/screen/UserFeedView.kt | 2 +- .../AccountSwitcherAndLeftDrawerLayout.kt | 6 +- .../ui/screen/loggedIn/AccountViewModel.kt | 4 +- .../ui/screen/loggedIn/LoggedInPage.kt | 2 +- .../loggedIn/bookmarks/BookmarkListScreen.kt | 4 +- .../loggedIn/chats/feed/ChatFeedView.kt | 2 +- .../loggedIn/chats/feed/ChatMessageCompose.kt | 6 +- .../loggedIn/chats/feed/DrawAuthorInfo.kt | 2 +- .../types/RenderChangeChannelMetadataNote.kt | 2 +- .../feed/types/RenderCreateChannelNote.kt | 7 +- .../chats/feed/types/RenderDraftEvent.kt | 2 +- .../chats/feed/types/RenderEncryptedFile.kt | 2 +- .../chats/feed/types/RenderRegularTextNote.kt | 2 +- .../chats/privateDM/ChatroomByAuthorScreen.kt | 2 +- .../chats/privateDM/ChatroomScreen.kt | 2 +- .../loggedIn/chats/privateDM/ChatroomView.kt | 2 +- .../privateDM/header/RenderRoomTopBar.kt | 4 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 93 ++++--------- .../send/PrivateMessageEditFieldRow.kt | 4 +- .../send/upload/ChatFileUploadDialog.kt | 2 +- .../publicChannels/ephemChat/ChannelView.kt | 2 +- .../ephemChat/EphemeralChatScreen.kt | 2 +- .../header/EphemeralChatChannelHeader.kt | 4 +- .../ephemChat/header/EphemeralChatTopBar.kt | 4 +- .../header/ShortEphemeralChatChannelHeader.kt | 3 +- .../header/actions/JoinChatButton.kt | 2 +- .../header/actions/LeaveChatButton.kt | 2 +- .../metadata/NewEphemeralChatScreen.kt | 69 ++-------- .../nip28PublicChat/ChannelView.kt | 2 +- .../PublicChatChannelHeader.kt | 2 +- .../PublicChatChannelScreen.kt | 2 +- .../header/LongPublicChatChannelHeader.kt | 2 +- .../header/PublicChatChannelHeader.kt | 4 +- .../header/PublicChatTopBar.kt | 4 +- .../header/ShortPublicChatChannelHeader.kt | 2 +- .../header/actions/EditChatButton.kt | 4 +- .../header/actions/JoinChatButton.kt | 2 +- .../header/actions/LeaveChatButton.kt | 2 +- .../header/actions/LinkChatButton.kt | 2 +- .../header/actions/OpenChatButton.kt | 2 +- .../header/actions/ShareChatButton.kt | 2 +- .../metadata/ChannelMetadataScreen.kt | 105 +++++---------- .../nip53LiveActivities/ChannelView.kt | 2 +- .../LiveActivitiesChannelHeader.kt | 4 +- .../LiveActivityChannelScreen.kt | 2 +- .../LongLiveActivityChannelHeader.kt | 4 +- .../ShortLiveActivityChannelHeader.kt | 2 +- .../header/LiveActivityTopBar.kt | 4 +- .../send/ChannelFileUploadDialog.kt | 2 +- .../chats/publicChannels/send/EditFieldRow.kt | 2 +- .../loggedIn/chats/rooms/ChannelFabColumn.kt | 6 +- .../chats/rooms/ChatroomHeaderCompose.kt | 6 +- .../loggedIn/chats/rooms/MessagesScreen.kt | 2 +- .../chats/rooms/feed/ChatroomListFeedView.kt | 2 +- .../chats/rooms/feed/ChatroomListTabs.kt | 2 +- .../rooms/singlepane/MessagesSinglePane.kt | 11 +- .../chats/rooms/twopane/ChatroomListPane.kt | 2 +- .../chats/rooms/twopane/MessagesTwoPane.kt | 14 +- .../chats/rooms/twopane/TwoPaneNav.kt | 9 +- .../chats/utils/ChatFileUploadDialog.kt | 11 +- .../chats/utils/DisplayReplyingToNote.kt | 2 +- .../loggedIn/communities/CommunityScreen.kt | 4 +- .../communities/NewCommunityNoteButton.kt | 4 +- .../loggedIn/discover/ChannelCardCompose.kt | 2 +- .../loggedIn/discover/DiscoverScreen.kt | 6 +- .../loggedIn/discover/DiscoveryTopBar.kt | 39 ++++-- .../discover/nip23LongForm/LongFormCard.kt | 2 +- .../RenderPublicChatChannelThumb.kt | 2 +- .../discover/nip51FollowSets/FollowSetCard.kt | 4 +- .../nip53LiveActivities/LiveActivityCard.kt | 2 +- .../nip72Communities/CommunityCard.kt | 2 +- .../loggedIn/discover/nip90DVMs/DVMCard.kt | 5 +- .../nip99Classifieds/ClassifiedsThumb.kt | 2 +- .../nip99Classifieds/NewProductScreen.kt | 60 +++------ .../screen/loggedIn/drafts/DraftListScreen.kt | 4 +- .../dvms/DvmContentDiscoveryScreen.kt | 2 +- .../ui/screen/loggedIn/dvms/DvmTopBar.kt | 4 +- .../loggedIn/geohash/GeoHashPostScreen.kt | 2 +- .../screen/loggedIn/geohash/GeoHashScreen.kt | 6 +- .../loggedIn/geohash/NewGeoNoteButton.kt | 4 +- .../GeoHashFilterAssemblerSubscription.kt | 2 +- .../loggedIn/hashtag/HashtagPostScreen.kt | 2 +- .../screen/loggedIn/hashtag/HashtagScreen.kt | 6 +- .../loggedIn/hashtag/NewHashtagNoteButton.kt | 4 +- .../HashtagFilterAssemblerSubscription.kt | 3 +- .../ui/screen/loggedIn/home/HomeScreen.kt | 6 +- .../ui/screen/loggedIn/home/HomeTopBar.kt | 32 ++++- .../ui/screen/loggedIn/home/NewNoteButton.kt | 4 +- .../loggedIn/home/ShortNotePostScreen.kt | 64 +++------ .../home/live/RenderEphemeralBubble.kt | 4 +- .../loggedIn/notifications/CardFeedView.kt | 2 +- .../notifications/NotificationScreen.kt | 6 +- .../notifications/NotificationTopBar.kt | 39 ++++-- .../loggedIn/privacy/PrivacyOptionsScreen.kt | 56 ++------ .../screen/loggedIn/profile/ProfileScreen.kt | 2 +- .../profile/bookmarks/TabBookmarks.kt | 2 +- .../conversations/TabNotesConversations.kt | 2 +- .../profile/followers/TabFollowers.kt | 2 +- .../loggedIn/profile/follows/TabFollows.kt | 2 +- .../profile/gallery/GalleryCardCompose.kt | 4 +- .../loggedIn/profile/gallery/GalleryThumb.kt | 2 +- .../profile/gallery/ProfileGalleryFeed.kt | 2 +- .../loggedIn/profile/gallery/TabGallery.kt | 2 +- .../profile/hashtags/TabFollowedTags.kt | 4 +- .../profile/header/DisplayLNAddress.kt | 4 +- .../profile/header/DrawAdditionalInfo.kt | 2 +- .../loggedIn/profile/header/EditButton.kt | 4 +- .../loggedIn/profile/header/MessageButton.kt | 4 +- .../loggedIn/profile/header/ProfileActions.kt | 2 +- .../loggedIn/profile/header/ProfileHeader.kt | 2 +- .../header/apps/DisplayAppRecommendations.kt | 2 +- .../loggedIn/profile/header/apps/WatchApp.kt | 4 +- .../profile/header/badges/DisplayBadges.kt | 4 +- .../profile/mutual/TabMutualConversations.kt | 2 +- .../profile/newthreads/TabNotesNewThreads.kt | 2 +- .../loggedIn/profile/relays/RelayFeedView.kt | 5 +- .../loggedIn/profile/relays/TabRelays.kt | 2 +- .../loggedIn/profile/reports/TabReports.kt | 2 +- .../loggedIn/profile/zaps/LnZapFeedView.kt | 2 +- .../loggedIn/profile/zaps/TabReceivedZaps.kt | 2 +- .../screen/loggedIn/qrcode/QrCodeScanner.kt | 2 +- .../ui/screen/loggedIn/qrcode/ShowQRDialog.kt | 2 +- .../loggedIn/redirect/LoadRedirectScreen.kt | 9 +- .../loggedIn/relays/AllRelayListScreen.kt | 75 ++++------- .../loggedIn/relays/RelayInformationScreen.kt | 2 +- .../relays/blocked/BlockedRelayListView.kt | 4 +- .../common/BasicRelaySetupInfoDialog.kt | 4 +- .../connected/ConnectedRelayListView.kt | 4 +- .../relays/dm/AddDMRelayListDialog.kt | 50 ++----- .../loggedIn/relays/dm/DMRelayListView.kt | 4 +- .../relays/local/LocalRelayListView.kt | 4 +- .../nip37/PrivateOutboxRelayListView.kt | 4 +- .../relays/nip65/Nip65RelayListView.kt | 4 +- .../relays/search/SearchRelayListView.kt | 4 +- .../relays/trusted/TrustedRelayListView.kt | 4 +- .../search/AddInboxRelayForSearchCard.kt | 4 +- .../search/AddSearchRelayListDialog.kt | 51 ++----- .../ui/screen/loggedIn/search/SearchScreen.kt | 8 +- .../loggedIn/settings/AppSettingsScreen.kt | 4 +- .../loggedIn/settings/NIP47SetupScreen.kt | 40 ++---- .../settings/SecurityFiltersScreen.kt | 4 +- .../loggedIn/settings/UserSettingsScreen.kt | 6 +- .../loggedIn/threadview/ThreadFeedView.kt | 8 +- .../loggedIn/threadview/ThreadScreen.kt | 4 +- .../screen/loggedIn/video/NewImageButton.kt | 2 +- .../ui/screen/loggedIn/video/StoriesTopBar.kt | 32 ++++- .../ui/screen/loggedIn/video/VideoScreen.kt | 8 +- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 2 - .../amethyst/ui/tor/TorSettingsDialog.kt | 60 ++++----- .../components/TranslatableRichTextViewer.kt | 2 +- 265 files changed, 1364 insertions(+), 1686 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/HeightDecreaser.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{NavBuilderEffects.kt => NavigationEffects.kt} (76%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{ => bottombars}/AppBottomBar.kt (67%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/BottomBarRoutes.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{ => drawer}/AccountSwitchBottomSheet.kt (99%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{ => drawer}/DrawerContent.kt (99%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{BottomBarRoute.kt => navs/INav.kt} (64%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{INav.kt => navs/Nav.kt} (53%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{NormalizedRelayUrlSerializer.kt => navs/RememberNavs.kt} (58%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{ => routes}/RouteMaker.kt (99%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{ => routes}/Routes.kt (94%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{ => topbars}/AmethystClickableIcon.kt (96%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{ => topbars}/FeedFilterSpinner.kt (99%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ShorterTopAppBar.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{ => topbars}/TopBarExtensibleWithBackButton.kt (96%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{ => topbars}/TopBarWithBackButton.kt (81%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/{AppTopBar.kt => topbars/UserDrawerSearchTopBar.kt} (69%) diff --git a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 0d080d7b03..b6e13bc24f 100644 --- a/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/amethyst/src/fdroid/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index bfba3d93fa..be838df741 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.screen.FeedDefinition import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent @@ -223,6 +224,10 @@ class AccountSettings( // list names // --- + fun changeDefaultHomeFollowList(name: FeedDefinition) { + changeDefaultHomeFollowList(name.code) + } + fun changeDefaultHomeFollowList(name: String) { if (defaultHomeFollowList.value != name) { defaultHomeFollowList.tryEmit(name) @@ -230,6 +235,10 @@ class AccountSettings( } } + fun changeDefaultStoriesFollowList(name: FeedDefinition) { + changeDefaultStoriesFollowList(name.code) + } + fun changeDefaultStoriesFollowList(name: String) { if (defaultStoriesFollowList.value != name) { defaultStoriesFollowList.tryEmit(name) @@ -237,6 +246,10 @@ class AccountSettings( } } + fun changeDefaultNotificationFollowList(name: FeedDefinition) { + changeDefaultNotificationFollowList(name.code) + } + fun changeDefaultNotificationFollowList(name: String) { if (defaultNotificationFollowList.value != name) { defaultNotificationFollowList.tryEmit(name) @@ -244,6 +257,10 @@ class AccountSettings( } } + fun changeDefaultDiscoveryFollowList(name: FeedDefinition) { + changeDefaultDiscoveryFollowList(name.code) + } + fun changeDefaultDiscoveryFollowList(name: String) { if (defaultDiscoveryFollowList.value != name) { defaultDiscoveryFollowList.tryEmit(name) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt index a78f944ef3..567b61ac3e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/HashtagIcon.kt @@ -49,7 +49,7 @@ import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment import com.vitorpamplona.amethyst.commons.richtext.RegularTextSegment import com.vitorpamplona.amethyst.ui.components.HashTag import com.vitorpamplona.amethyst.ui.components.RenderRegular -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt index cbcdcb88eb..dbd28b2dd8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/DisplayNotifyMessages.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyRequestsCache -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt index 28178a220c..c87d634923 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/notifyCommand/compose/NotifyRequestDialog.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size16dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 74affd46fb..ca69bdf2ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -39,8 +39,8 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.AccountScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.prepareSharedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 67a6a9f682..66f922c901 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.foundation.border import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -50,8 +49,6 @@ import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf @@ -84,10 +81,9 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.BechLink import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription @@ -98,7 +94,6 @@ import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size5dp -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.replyModifier @@ -138,45 +133,22 @@ fun EditPostView( ) { Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = StdHorzSpacer) - - PostButton( - onPost = { - postViewModel.sendPost() - scope.launch { - delay(100) - onClose() - } - }, - isActive = postViewModel.canPost(), - ) + PostingTopBar( + isActive = postViewModel::canPost, + onPost = { + postViewModel.sendPost() + scope.launch { + delay(100) + onClose() } }, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - postViewModel.cancel() - scope.launch { - delay(100) - onClose() - } - }, - ) + onCancel = { + postViewModel.cancel() + scope.launch { + delay(100) + onClose() } }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt index f5a6a49e84..643a72ace7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaView.kt @@ -23,8 +23,6 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -41,14 +39,11 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -63,9 +58,8 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.PostButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner @@ -73,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size5dp -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -108,43 +101,20 @@ fun NewMediaView( SetDialogToEdgeToEdge() Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = StdHorzSpacer) - - PostButton( - onPost = { - postViewModel.upload(context, onClose, accountViewModel.toastManager::toast) - postViewModel.selectedServer?.let { - if (it.type != ServerType.NIP95) { - account.settings.changeDefaultFileServer(it) - } - } - }, - isActive = postViewModel.canPost(), - ) + PostingTopBar( + isActive = postViewModel::canPost, + onCancel = { + postViewModel.cancelModel() + onClose() + }, + onPost = { + postViewModel.upload(context, onClose, accountViewModel.toastManager::toast) + postViewModel.selectedServer?.let { + if (it.type != ServerType.NIP95) { + account.settings.changeDefaultFileServer(it) + } } }, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - postViewModel.cancelModel() - onClose() - }, - ) - } - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> @@ -155,8 +125,16 @@ fun NewMediaView( .consumeWindowInsets(pad) .imePadding(), ) { - Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp, bottom = 10.dp)) { - Column(Modifier.fillMaxWidth().verticalScroll(scrollState)) { + Column( + Modifier + .fillMaxSize() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + ) { + Column( + Modifier + .fillMaxWidth() + .verticalScroll(scrollState), + ) { ImageVideoPost(postViewModel, accountViewModel) } } @@ -196,7 +174,11 @@ fun ImageVideoPost( OutlinedTextField( label = { Text(text = stringRes(R.string.add_caption)) }, - modifier = Modifier.fillMaxWidth().padding(top = 3.dp).height(150.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(top = 3.dp) + .height(150.dp), maxLines = 10, value = postViewModel.caption, onValueChange = { postViewModel.caption = it }, @@ -215,7 +197,10 @@ fun ImageVideoPost( SettingSwitchItem( title = R.string.add_sensitive_content_label, description = R.string.add_sensitive_content_description, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp), checked = postViewModel.sensitiveContent, onCheckedChange = { postViewModel.sensitiveContent = it }, ) @@ -234,7 +219,10 @@ fun ImageVideoPost( } Column( - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(Size5dp), ) { Text( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt index 7338a622c1..ddb143880f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt @@ -20,9 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.actions -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth @@ -37,27 +35,20 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.MinHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText @OptIn(ExperimentalMaterial3Api::class) @@ -75,48 +66,16 @@ fun NewUserMetadataScreen( Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = MinHorzSpacer) - - Text( - text = stringRes(R.string.profile), - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - - SaveButton( - onPost = { - postViewModel.create() - nav.popBack() - }, - true, - ) - } + SavingTopBar( + titleRes = R.string.profile, + onCancel = { + postViewModel.clear() + nav.popBack() }, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - postViewModel.clear() - nav.popBack() - }, - ) - } + onPost = { + postViewModel.create() + nav.popBack() }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt index f89bb83ab2..98887780e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersLIstView.kt @@ -24,10 +24,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -35,113 +32,29 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategoryWithButton import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.grayText -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MediaServersListView( - nip96ServersViewModel: NIP96ServersViewModel, - blossomServersViewModel: BlossomServersViewModel, - onClose: () -> Unit, -) { - Scaffold( - topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceAround, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringRes(id = R.string.media_servers), - style = MaterialTheme.typography.titleLarge, - ) - } - }, - navigationIcon = { - CloseButton( - modifier = HalfHorzPadding, - onPress = { - nip96ServersViewModel.refresh() - blossomServersViewModel.refresh() - onClose() - }, - ) - }, - actions = { - SaveButton( - modifier = HalfHorzPadding, - onPost = { - nip96ServersViewModel.saveFileServers() - blossomServersViewModel.saveFileServers() - onClose() - }, - isActive = true, - ) - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), - ) - }, - ) { padding -> - Column( - modifier = - Modifier - .fillMaxSize() - .padding( - start = 16.dp, - top = padding.calculateTopPadding(), - end = 16.dp, - bottom = padding.calculateBottomPadding(), - ).consumeWindowInsets(padding) - .imePadding(), - verticalArrangement = Arrangement.spacedBy(10.dp, alignment = Alignment.Top), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - stringRes(id = R.string.set_preferred_media_servers), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.grayText, - ) - - AllMediaBody(nip96ServersViewModel, blossomServersViewModel) - } - } -} - @Composable fun AllMediaBody( nip96ServersViewModel: NIP96ServersViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt index 17be2ff5b8..587fab25fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/AllMediaServersScreen.kt @@ -22,18 +22,14 @@ package com.vitorpamplona.amethyst.ui.actions.mediaServers import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -42,12 +38,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding import com.vitorpamplona.amethyst.ui.theme.grayText @Composable @@ -77,44 +71,18 @@ fun MediaServersScaffold( ) { Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceAround, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringRes(id = R.string.media_servers), - style = MaterialTheme.typography.titleLarge, - ) - } + SavingTopBar( + titleRes = R.string.media_servers, + onCancel = { + nip96ServersViewModel.refresh() + blossomServersViewModel.refresh() + onClose() }, - navigationIcon = { - CloseButton( - modifier = HalfHorzPadding, - onPress = { - nip96ServersViewModel.refresh() - blossomServersViewModel.refresh() - onClose() - }, - ) + onPost = { + nip96ServersViewModel.saveFileServers() + blossomServersViewModel.saveFileServers() + onClose() }, - actions = { - SaveButton( - modifier = HalfHorzPadding, - isActive = true, - onPost = { - nip96ServersViewModel.saveFileServers() - blossomServersViewModel.saveFileServers() - onClose() - }, - ) - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { padding -> @@ -133,8 +101,9 @@ fun MediaServersScaffold( horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - stringRes(id = R.string.set_preferred_media_servers), + text = stringRes(id = R.string.set_preferred_media_servers), textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 10.dp), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.grayText, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index 8c4a7b8458..cdf010fb39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -58,9 +58,9 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.njumpLink import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt index 77ebb7bfe3..6d6b55a9c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ExpandableRichTextViewer.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.richtext.ExpandableTextCutOffCalculator -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 5670c32667..6a0cc02f3c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -88,10 +88,10 @@ import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.creators.invoice.MayBeInvoicePreview import com.vitorpamplona.amethyst.ui.note.toShortDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt index 9260f1b585..70468d31ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/MarkdownMediaRenderer.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.amethyst.ui.components.DisplayFullNote import com.vitorpamplona.amethyst.ui.components.DisplayUser import com.vitorpamplona.amethyst.ui.components.LoadUrlPreview import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadedBechLink import com.vitorpamplona.amethyst.ui.theme.Font17SP diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt index 59ef87311e..a70b427f20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/markdown/RenderContentAsMarkdown.kt @@ -41,8 +41,8 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.service.previews.UrlInfoItem import com.vitorpamplona.amethyst.ui.components.UrlPreviewState -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt index abbf31e51a..2f0fd947af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/DisplayErrorMessages.kt @@ -25,7 +25,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiErrorToastMsg import com.vitorpamplona.amethyst.ui.components.toasts.multiline.MultiUserErrorMessageDialog -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt index 826f216876..ae1d16be90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/ErrorList.kt @@ -44,9 +44,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeToMessage +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt index b4546a4bc0..85e7cc817d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/toasts/multiline/MultiUserErrorMessageDialog.kt @@ -33,8 +33,8 @@ import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt index f0d80ffba6..31aa9f1b46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentStateView.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt index d3dadab569..eae3d24525 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedLoaded.kt @@ -32,7 +32,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 2970449099..729af27ec3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -26,12 +26,6 @@ import android.os.Parcelable import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.slideOutVertically import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -51,6 +45,12 @@ import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataScreen import com.vitorpamplona.amethyst.ui.actions.mediaServers.AllMediaServersScreen import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.components.toasts.DisplayErrorMessages +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberNav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.getRouteWithArguments +import com.vitorpamplona.amethyst.ui.navigation.routes.isBaseRoute +import com.vitorpamplona.amethyst.ui.navigation.routes.isSameRoute import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel @@ -381,36 +381,6 @@ private fun NavigateIfIntentRequested( } } -private fun isSameRoute( - currentRoute: Route?, - newRoute: Route, -): Boolean { - if (currentRoute == null) return false - - if (currentRoute == newRoute) { - return true - } - - if (newRoute is Route.EventRedirect) { - return when (currentRoute) { - is Route.Note -> newRoute.id == currentRoute.id - is Route.PublicChatChannel -> newRoute.id == currentRoute.id - else -> false - } - } - - return false -} - -val slideInVerticallyFromBottom = slideInVertically(animationSpec = tween(), initialOffsetY = { it }) -val slideOutVerticallyToBottom = slideOutVertically(animationSpec = tween(), targetOffsetY = { it }) - -val slideInHorizontallyFromEnd = slideInHorizontally(animationSpec = tween(), initialOffsetX = { it }) -val slideOutHorizontallyToEnd = slideOutHorizontally(animationSpec = tween(), targetOffsetX = { it }) - -val scaleIn = scaleIn(animationSpec = tween(), initialScale = 0.9f) -val scaleOut = scaleOut(animationSpec = tween(), targetScale = 0.9f) - fun URI.findParameterValue(parameterName: String): String? = rawQuery ?.split('&') diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/HeightDecreaser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/HeightDecreaser.kt deleted file mode 100644 index 2c28c21e94..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/HeightDecreaser.kt +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.navigation - -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.DecayAnimationSpec -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.TopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.platform.LocalDensity -import com.vitorpamplona.amethyst.ui.theme.TopBarSize - -// This is a hack to decrease the height for the -// default TopBar without having to reimplement it. - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun rememberHeightDecreaser(): TopAppBarScrollBehavior { - val height = - LocalDensity.current.run { - TopBarSize.toPx() - } - - return remember(height) { - HeightDecreaser(height) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -class HeightDecreaser( - height: Float, -) : TopAppBarScrollBehavior { - override val state: TopAppBarState = - TopAppBarState( - initialHeightOffsetLimit = -Float.MAX_VALUE, - initialHeightOffset = 0f, - initialContentOffset = 0f, - ).also { - it.heightOffset = height - } - - override val isPinned: Boolean = true - override val snapAnimationSpec: AnimationSpec? = null - override val flingAnimationSpec: DecayAnimationSpec? = null - override var nestedScrollConnection = - object : NestedScrollConnection { - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource, - ): Offset = available - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavBuilderEffects.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt similarity index 76% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavBuilderEffects.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt index 120331cd29..caf2f71c64 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavBuilderEffects.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NavigationEffects.kt @@ -21,6 +21,13 @@ package com.vitorpamplona.amethyst.ui.navigation import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically import androidx.compose.runtime.Composable import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraphBuilder @@ -64,3 +71,12 @@ inline fun NavGraphBuilder.composableArgs(noinline content: @C content(it.toRoute()) } } + +val slideInVerticallyFromBottom = slideInVertically(animationSpec = tween(), initialOffsetY = { it }) +val slideOutVerticallyToBottom = slideOutVertically(animationSpec = tween(), targetOffsetY = { it }) + +val slideInHorizontallyFromEnd = slideInHorizontally(animationSpec = tween(), initialOffsetX = { it }) +val slideOutHorizontallyToEnd = slideOutHorizontally(animationSpec = tween(), targetOffsetX = { it }) + +val scaleIn = scaleIn(animationSpec = tween(), initialScale = 0.9f) +val scaleOut = scaleOut(animationSpec = tween(), targetScale = 0.9f) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt similarity index 67% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt index 2c6eed373d..3ca3ad65dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt @@ -18,11 +18,8 @@ * 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.navigation +package com.vitorpamplona.amethyst.ui.navigation.bottombars -import android.graphics.Rect -import android.view.View -import android.view.ViewTreeObserver import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -30,7 +27,6 @@ import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.BottomAppBarDefaults.windowInsets import androidx.compose.material3.HorizontalDivider @@ -39,84 +35,19 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.State import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size0dp import com.vitorpamplona.amethyst.ui.theme.Size10Modifier -import com.vitorpamplona.amethyst.ui.theme.Size24dp -import com.vitorpamplona.amethyst.ui.theme.Size25dp -import kotlinx.collections.immutable.persistentListOf - -val bottomNavigationItems = - persistentListOf( - BottomBarRoute(Route.Home, R.drawable.ic_home, R.string.route_home, Modifier.size(Size25dp), Modifier.size(Size24dp)), - BottomBarRoute(Route.Message, R.drawable.ic_dm, R.string.route_messages), - BottomBarRoute(Route.Video, R.drawable.ic_video, R.string.route_video), - BottomBarRoute(Route.Discover, R.drawable.ic_sensors, R.string.route_discover), - BottomBarRoute(Route.Notification, R.drawable.ic_notifications, R.string.route_notifications), - ) - -enum class Keyboard { - Opened, - Closed, -} - -fun isKeyboardOpen(view: View): Keyboard { - val rect = Rect() - view.getWindowVisibleDisplayFrame(rect) - val screenHeight = view.rootView.height - val keypadHeight = screenHeight - rect.bottom - - return if (keypadHeight > screenHeight * 0.15) { - Keyboard.Opened - } else { - Keyboard.Closed - } -} - -@Composable -fun keyboardAsState(): State { - val view = LocalView.current - - val keyboardState = remember(view) { mutableStateOf(isKeyboardOpen(view)) } - - DisposableEffect(view) { - val onGlobalListener = - ViewTreeObserver.OnGlobalLayoutListener { - val newKeyboardValue = isKeyboardOpen(view) - - if (newKeyboardValue != keyboardState.value) { - keyboardState.value = newKeyboardValue - } - } - view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) - onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) } - } - - return keyboardState -} - -@Composable -fun IfKeyboardClosed(inner: @Composable () -> Unit) { - val isKeyboardState by keyboardAsState() - if (isKeyboardState == Keyboard.Closed) { - inner() - } -} @Composable fun AppBottomBar( @@ -124,7 +55,10 @@ fun AppBottomBar( accountViewModel: AccountViewModel, nav: (Route) -> Unit, ) { - IfKeyboardClosed { RenderBottomMenu(selectedRoute, accountViewModel, nav) } + val isKeyboardState by keyboardAsState() + if (isKeyboardState == KeyboardState.Closed) { + RenderBottomMenu(selectedRoute, accountViewModel, nav) + } } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/BottomBarRoutes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/BottomBarRoutes.kt new file mode 100644 index 0000000000..16dc2c9905 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/BottomBarRoutes.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.navigation.bottombars + +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.theme.Size20dp +import com.vitorpamplona.amethyst.ui.theme.Size23dp +import com.vitorpamplona.amethyst.ui.theme.Size24dp +import com.vitorpamplona.amethyst.ui.theme.Size25dp +import kotlinx.collections.immutable.persistentListOf + +class BottomBarRoute( + val route: Route, + val icon: Int, + val contentDescriptor: Int, + val notifSize: Modifier = Modifier.size(Size23dp), + val iconSize: Modifier = Modifier.size(Size20dp), +) + +val bottomNavigationItems = + persistentListOf( + BottomBarRoute(Route.Home, R.drawable.ic_home, R.string.route_home, Modifier.size(Size25dp), Modifier.size(Size24dp)), + BottomBarRoute(Route.Message, R.drawable.ic_dm, R.string.route_messages), + BottomBarRoute(Route.Video, R.drawable.ic_video, R.string.route_video), + BottomBarRoute(Route.Discover, R.drawable.ic_sensors, R.string.route_discover), + BottomBarRoute(Route.Notification, R.drawable.ic_notifications, R.string.route_notifications), + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt new file mode 100644 index 0000000000..ce874a519e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/KeyboardState.kt @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.navigation.bottombars + +import android.graphics.Rect +import android.view.View +import android.view.ViewTreeObserver +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalView + +enum class KeyboardState { + Opened, + Closed, +} + +@Composable +fun keyboardAsState(): State { + val view = LocalView.current + + val keyboardState = remember(view) { mutableStateOf(isKeyboardOpen(view)) } + + DisposableEffect(view) { + val onGlobalListener = + ViewTreeObserver.OnGlobalLayoutListener { + val newKeyboardValue = isKeyboardOpen(view) + + if (newKeyboardValue != keyboardState.value) { + keyboardState.value = newKeyboardValue + } + } + view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) + onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) } + } + + return keyboardState +} + +fun isKeyboardOpen(view: View): KeyboardState { + val rect = Rect() + view.getWindowVisibleDisplayFrame(rect) + val screenHeight = view.rootView.height + val keypadHeight = screenHeight - rect.bottom + + return if (keypadHeight > screenHeight * 0.15) { + KeyboardState.Opened + } else { + KeyboardState.Closed + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt index 631b36d75a..ca1f0cb773 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/AccountSwitchBottomSheet.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.navigation +package com.vitorpamplona.amethyst.ui.navigation.drawer import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index 37026ed367..03a1fe521f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.navigation +package com.vitorpamplona.amethyst.ui.navigation.drawer import androidx.compose.foundation.Image import androidx.compose.foundation.border @@ -98,6 +98,9 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.LoadStatuses import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt new file mode 100644 index 0000000000..8e3324ae6b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/EmptyNav.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.navigation.navs + +import androidx.compose.material3.DrawerState +import androidx.compose.material3.DrawerValue +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.runBlocking +import kotlin.reflect.KClass + +@Stable +object EmptyNav : INav { + override val scope: CoroutineScope get() = TODO("Not yet implemented") + override val drawerState = DrawerState(DrawerValue.Closed) + + override fun closeDrawer() = runBlocking { drawerState.close() } + + override fun openDrawer() = runBlocking { drawerState.open() } + + override fun nav(route: Route) {} + + override fun nav(computeRoute: suspend () -> Route?) {} + + override fun newStack(route: Route) {} + + override fun popBack() {} + + override fun popUpTo( + route: Route, + klass: KClass, + ) {} +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/BottomBarRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt similarity index 64% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/BottomBarRoute.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt index dacf6b471d..aefcb060cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/BottomBarRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/INav.kt @@ -18,17 +18,33 @@ * 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.navigation +package com.vitorpamplona.amethyst.ui.navigation.navs -import androidx.compose.foundation.layout.size -import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.ui.theme.Size20dp -import com.vitorpamplona.amethyst.ui.theme.Size23dp +import androidx.compose.material3.DrawerState +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import kotlinx.coroutines.CoroutineScope +import kotlin.reflect.KClass -class BottomBarRoute( - val route: Route, - val icon: Int, - val contentDescriptor: Int, - val notifSize: Modifier = Modifier.size(Size23dp), - val iconSize: Modifier = Modifier.size(Size20dp), -) +@Stable +interface INav { + val scope: CoroutineScope + val drawerState: DrawerState + + fun closeDrawer() + + fun openDrawer() + + fun nav(route: Route) + + fun nav(computeRoute: suspend () -> Route?) + + fun newStack(route: Route) + + fun popBack() + + fun popUpTo( + route: Route, + klass: KClass, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/INav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt similarity index 53% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/INav.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt index 868cc8425b..fa3b50e994 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/INav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/Nav.kt @@ -18,64 +18,23 @@ * 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.navigation +package com.vitorpamplona.amethyst.ui.navigation.navs import android.annotation.SuppressLint import androidx.compose.material3.DrawerState import androidx.compose.material3.DrawerValue -import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.getRouteWithArguments import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlin.reflect.KClass -@Composable -fun rememberNav(): Nav { - val navController = rememberNavController() - val scope = rememberCoroutineScope() - - return remember(navController, scope) { - Nav(navController, scope) - } -} - -@Composable -fun rememberExtendedNav( - nav: INav, - onClose: () -> Unit, -): INav = nav.onNavigate(onClose) - -@Stable -interface INav { - val drawerState: DrawerState - - fun nav(route: Route) - - fun nav(computeRoute: suspend () -> Route?) - - fun newStack(route: Route) - - fun popBack() - - fun popUpTo( - route: Route, - klass: KClass, - ) - - fun closeDrawer() - - fun openDrawer() -} - @Stable class Nav( val controller: NavHostController, - val scope: CoroutineScope, + override val scope: CoroutineScope, ) : INav { override val drawerState = DrawerState(DrawerValue.Closed) @@ -124,83 +83,12 @@ class Nav( @SuppressLint("RestrictedApi") override fun popUpTo( route: Route, - upToClass: KClass, + klass: KClass, ) { scope.launch { controller.navigate(route) { - popUpTo(upToClass) { inclusive = true } + popUpTo(klass) { inclusive = true } } } } } - -@Stable -object EmptyNav : INav { - override val drawerState = DrawerState(DrawerValue.Closed) - - override fun closeDrawer() { - runBlocking { drawerState.close() } - } - - override fun openDrawer() { - runBlocking { drawerState.open() } - } - - override fun nav(route: Route) {} - - override fun nav(computeRoute: suspend () -> Route?) {} - - override fun newStack(route: Route) {} - - override fun popBack() {} - - override fun popUpTo( - route: Route, - upToClass: KClass, - ) {} -} - -fun INav.onNavigate(runOnNavigate: () -> Unit): INav = ObservableNavigate(this, runOnNavigate) - -class ObservableNavigate( - val nav: INav, - val onNavigate: () -> Unit, -) : INav { - override val drawerState: DrawerState = nav.drawerState - - override fun closeDrawer() { - nav.closeDrawer() - } - - override fun openDrawer() { - nav.openDrawer() - } - - override fun nav(route: Route) { - onNavigate() - nav.nav(route) - } - - override fun nav(computeRoute: suspend () -> Route?) { - onNavigate() - nav.nav(computeRoute) - } - - override fun newStack(route: Route) { - onNavigate() - nav.newStack(route) - } - - override fun popBack() { - onNavigate() - nav.popBack() - } - - override fun popUpTo( - route: Route, - upToClass: KClass, - ) { - onNavigate() - nav.popUpTo(route, upToClass) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt new file mode 100644 index 0000000000..c1ecb01995 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/ObservableNav.kt @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.navigation.navs + +import androidx.compose.material3.DrawerState +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlin.reflect.KClass + +class ObservableNav( + val sourceNav: INav, + override val scope: CoroutineScope, + val onBeforeNavigate: () -> Unit, +) : INav { + override val drawerState: DrawerState = sourceNav.drawerState + + override fun closeDrawer() { + sourceNav.closeDrawer() + } + + override fun openDrawer() { + sourceNav.openDrawer() + } + + override fun nav(route: Route) { + scope.launch { + onBeforeNavigate() + } + sourceNav.nav(route) + } + + override fun nav(computeRoute: suspend () -> Route?) { + scope.launch { + onBeforeNavigate() + } + sourceNav.nav(computeRoute) + } + + override fun newStack(route: Route) { + scope.launch { + onBeforeNavigate() + } + sourceNav.newStack(route) + } + + override fun popBack() { + scope.launch { + onBeforeNavigate() + } + sourceNav.popBack() + } + + override fun popUpTo( + route: Route, + upToClass: KClass, + ) { + scope.launch { + onBeforeNavigate() + } + sourceNav.popUpTo(route, upToClass) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt similarity index 58% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt index 10a0446c6d..218f5ae927 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/NormalizedRelayUrlSerializer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/navs/RememberNavs.kt @@ -18,24 +18,30 @@ * 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.navigation +package com.vitorpamplona.amethyst.ui.navigation.navs -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.navigation.compose.rememberNavController -object NormalizedRelayUrlSerializer : KSerializer { - override val descriptor = PrimitiveSerialDescriptor("NormalizedRelayUrl", PrimitiveKind.STRING) +@Composable +fun rememberNav(): Nav { + val navController = rememberNavController() + val scope = rememberCoroutineScope() - override fun deserialize(decoder: Decoder): NormalizedRelayUrl = NormalizedRelayUrl(decoder.decodeString()) - - override fun serialize( - encoder: Encoder, - value: NormalizedRelayUrl, - ) { - encoder.encodeString(value.url) + return remember(navController, scope) { + Nav(navController, scope) + } +} + +@Composable +fun rememberExtendedNav( + nav: INav, + onBeforeNavigate: () -> Unit, +): INav { + val scope = rememberCoroutineScope() + return remember(nav, scope) { + ObservableNav(nav, scope, onBeforeNavigate) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 831e692c8d..81d2d0ee0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.navigation +package com.vitorpamplona.amethyst.ui.navigation.routes import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index 4c7a06bb8f..bd9d01613e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.navigation +package com.vitorpamplona.amethyst.ui.navigation.routes import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavHostController @@ -237,3 +237,24 @@ fun getRouteWithArguments(navController: NavHostController): Route? { } } } + +fun isSameRoute( + currentRoute: Route?, + newRoute: Route, +): Boolean { + if (currentRoute == null) return false + + if (currentRoute == newRoute) { + return true + } + + if (newRoute is Route.EventRedirect) { + return when (currentRoute) { + is Route.Note -> newRoute.id == currentRoute.id + is Route.PublicChatChannel -> newRoute.id == currentRoute.id + else -> false + } + } + + return false +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt new file mode 100644 index 0000000000..cb9cea8e5d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.navigation.topbars + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ActionTopBar( + postRes: Int, + titleRes: Int? = null, + isActive: () -> Boolean = { true }, + onCancel: () -> Unit, + onPost: () -> Unit, +) { + ShorterTopAppBar( + title = { + if (titleRes != null) { + Text( + text = stringRes(titleRes), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + }, + navigationIcon = { + CloseButton( + modifier = HalfHorzPadding, + onPress = onCancel, + ) + }, + actions = { + Button( + modifier = HalfHorzPadding, + enabled = isActive(), + onClick = onPost, + ) { + Text(text = stringRes(postRes)) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PostingTopBar( + titleRes: Int? = null, + isActive: () -> Boolean = { true }, + onCancel: () -> Unit, + onPost: () -> Unit, +) = ActionTopBar( + titleRes = titleRes, + postRes = R.string.post, + isActive = isActive, + onCancel = onCancel, + onPost = onPost, +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SavingTopBar( + titleRes: Int? = null, + isActive: () -> Boolean = { true }, + onCancel: () -> Unit, + onPost: () -> Unit, +) = ActionTopBar( + titleRes = titleRes, + postRes = R.string.save, + isActive = isActive, + onCancel = onCancel, + onPost = onPost, +) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CreatingTopBar( + titleRes: Int? = null, + isActive: () -> Boolean = { true }, + onCancel: () -> Unit, + onPost: () -> Unit, +) = ActionTopBar( + titleRes = titleRes, + postRes = R.string.create, + isActive = isActive, + onCancel = onCancel, + onPost = onPost, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AmethystClickableIcon.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AmethystClickableIcon.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AmethystClickableIcon.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AmethystClickableIcon.kt index 656bdd7b11..0d1584ff89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AmethystClickableIcon.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/AmethystClickableIcon.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.navigation +package com.vitorpamplona.amethyst.ui.navigation.topbars import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt similarity index 99% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index 44640dd41c..961448f849 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.navigation +package com.vitorpamplona.amethyst.ui.navigation.topbars import android.Manifest import androidx.compose.foundation.clickable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ShorterTopAppBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ShorterTopAppBar.kt new file mode 100644 index 0000000000..8f8645eca1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ShorterTopAppBar.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.navigation.topbars + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +val TopBarSize = 50.dp + +@ExperimentalMaterial3Api +@Composable +fun ShorterTopAppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + expandedHeight: Dp = TopBarSize, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(), + scrollBehavior: TopAppBarScrollBehavior? = null, +) = TopAppBar( + title, + modifier, + navigationIcon, + actions, + expandedHeight, + windowInsets, + colors, + scrollBehavior, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/TopBarExtensibleWithBackButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/TopBarExtensibleWithBackButton.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/TopBarExtensibleWithBackButton.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/TopBarExtensibleWithBackButton.kt index 3590b230ff..9b652d99cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/TopBarExtensibleWithBackButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/TopBarExtensibleWithBackButton.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.navigation +package com.vitorpamplona.amethyst.ui.navigation.topbars import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -31,7 +31,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -69,8 +68,7 @@ fun MyExtensibleTopAppBar( Column( Modifier.clickable { expanded.value = !expanded.value }, ) { - TopAppBar( - scrollBehavior = rememberHeightDecreaser(), + ShorterTopAppBar( title = { Row( verticalAlignment = Alignment.CenterVertically, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/TopBarWithBackButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/TopBarWithBackButton.kt similarity index 81% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/TopBarWithBackButton.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/TopBarWithBackButton.kt index 9e23157039..2a0b46ba7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/TopBarWithBackButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/TopBarWithBackButton.kt @@ -18,14 +18,13 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.navigation +package com.vitorpamplona.amethyst.ui.navigation.topbars import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon @OptIn(ExperimentalMaterial3Api::class) @@ -34,17 +33,18 @@ fun TopBarWithBackButton( caption: String, popBack: () -> Unit, ) { - TopAppBar( - scrollBehavior = rememberHeightDecreaser(), - title = { Text(caption) }, + ShorterTopAppBar( + title = { + Text( + text = caption, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + }, navigationIcon = { - IconButton( - onClick = popBack, - modifier = Modifier, - ) { + IconButton(popBack) { ArrowBackIcon() } }, - actions = {}, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt similarity index 69% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt index b589ef8464..f5bfc66605 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/UserDrawerSearchTopBar.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.navigation +package com.vitorpamplona.amethyst.ui.navigation.topbars import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -26,43 +26,32 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserPicture import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.SearchIcon -import com.vitorpamplona.amethyst.ui.screen.FeedDefinition -import com.vitorpamplona.amethyst.ui.screen.FollowListState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier import com.vitorpamplona.amethyst.ui.theme.Size22Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText -@Composable -fun MainTopBar( - accountViewModel: AccountViewModel, - nav: INav, -) { - GenericMainTopBar(accountViewModel, nav) { AmethystClickableIcon() } -} - @OptIn(ExperimentalMaterial3Api::class) @Composable -fun GenericMainTopBar( +fun UserDrawerSearchTopBar( accountViewModel: AccountViewModel, nav: INav, content: @Composable () -> Unit, ) { - TopAppBar( - scrollBehavior = rememberHeightDecreaser(), + ShorterTopAppBar( title = { Column( modifier = Modifier.fillMaxWidth(), @@ -102,39 +91,3 @@ private fun LoggedInUserPictureDrawer( ) } } - -@Composable -fun FollowListWithRoutes( - followListsModel: FollowListState, - listName: String, - accountViewModel: AccountViewModel, - onChange: (FeedDefinition) -> Unit, -) { - val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle() - - FeedFilterSpinner( - placeholderCode = listName, - explainer = stringRes(R.string.select_list_to_filter), - options = allLists, - onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow) }, - accountViewModel = accountViewModel, - ) -} - -@Composable -fun FollowListWithoutRoutes( - followListsModel: FollowListState, - listName: String, - accountViewModel: AccountViewModel, - onChange: (FeedDefinition) -> Unit, -) { - val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle() - - FeedFilterSpinner( - placeholderCode = listName, - explainer = stringRes(R.string.select_list_to_filter), - options = allLists, - onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow) }, - accountViewModel = accountViewModel, - ) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index 6d176cdda8..b2f984ffaf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -43,8 +43,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt index 3afde97560..35497746eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlankNote.kt @@ -37,8 +37,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt index 7d35a08634..bdcfa1d943 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BlockReportChecker.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt index 61e10c3d28..25f1c7e159 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Gallery.kt @@ -36,8 +36,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Size25dp import kotlinx.collections.immutable.ImmutableList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index 92e50947d5..7ad06f15b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 0fd1c2e5d6..04b114d9e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -74,9 +74,9 @@ import com.vitorpamplona.amethyst.ui.components.CoreSecretMessage import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.authorRouteFor -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.authorRouteFor +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.elements.NoteDropDownMenu import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index b859a1d38c..3b91d38691 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -60,8 +60,8 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserNip05 import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.WatchAuthor import com.vitorpamplona.amethyst.ui.painterRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index d70b05343b..61ffa5e0ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -62,8 +62,8 @@ import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.layouts.GenericRepostLayout -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.DisplayZapSplits import com.vitorpamplona.amethyst.ui.note.elements.BoostedMark import com.vitorpamplona.amethyst.ui.note.elements.DisplayEditStatus diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index ffc2c7ff93..dace09a023 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -82,9 +82,9 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.report.ReportNoteDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index bca63658b3..35a4ae7da3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -78,9 +78,9 @@ import com.vitorpamplona.amethyst.service.ZapPaymentHandler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.toasts.StringToastMsg -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeToMessage +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockVitorAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 244bd30c92..56ad6b551f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -113,9 +113,9 @@ import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeReplyTo +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeReplyTo import com.vitorpamplona.amethyst.ui.note.types.EditState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt index 7f9f126045..27bfd4599a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt @@ -52,7 +52,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt index 110aacf9f7..f67138bb5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -39,7 +39,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -55,8 +54,8 @@ import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt index 0c2377b1e3..d3392b4f40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReplyInformation.kt @@ -38,8 +38,8 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index 1f4fa9947c..38d0e45031 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -51,7 +51,6 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue @@ -82,15 +81,13 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.ui.components.InLineIconRenderer import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.note.types.RenderEmojiPack import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonBorder -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -190,28 +187,15 @@ fun UpdateReactionTypeDialog( Scaffold( topBar = { - TopAppBar( - actions = { - SaveButton( - onPost = { - postViewModel.sendPost() - onClose() - }, - isActive = postViewModel.hasChanged(), - ) - Spacer(modifier = StdHorzSpacer) + SavingTopBar( + isActive = postViewModel::hasChanged, + onCancel = { + postViewModel.cancel() + onClose() }, - title = {}, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - postViewModel.cancel() - onClose() - }, - ) - } + onPost = { + postViewModel.sendPost() + onClose() }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt index 59282e5704..7e862d7af5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserCompose.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.StdPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index cf3dae4e1b..3cfbaec4af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -42,8 +42,8 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index 6044d99146..6dd97fd556 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -46,8 +46,8 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observe import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserAboutMe import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowing -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt index 1c75337450..044e66a493 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapUserSetCompose.kt @@ -34,8 +34,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.ZapUserSetCard import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt index b624e54594..87ddf7a021 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/DisplayPreviews.kt @@ -28,7 +28,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.FillWidthQuoteBorderModifier import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt index 286b57aebf..a0783d262e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt @@ -50,7 +50,7 @@ import com.vitorpamplona.amethyst.ui.components.DisplayUrlWithLoadingSymbol import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.components.WaitAndDisplay -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlin.text.startsWith diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/DisplayZapSplits.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/DisplayZapSplits.kt index 1e7d8aca28..52c1b032e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/DisplayZapSplits.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/zapsplits/DisplayZapSplits.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt index c5ba242ba3..45684c691d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/AddInboxRelayForDMCard.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt index e27ef8051f..877b417d41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayCommunity.kt @@ -30,8 +30,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.buildLinkString -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt index 04bb1a78c3..13a3f70c0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayHashtags.kt @@ -37,8 +37,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.buildLinkString -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.hashtags.firstIsTaggedHashes import com.vitorpamplona.quartz.nip22Comments.CommentEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt index 2a6dd3af22..33a443a547 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayLocation.kt @@ -28,8 +28,8 @@ import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withLink -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Font14SP diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index 40d9e0c499..e1e0609409 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -59,8 +59,8 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteReplies import com.vitorpamplona.amethyst.ui.components.ClickableTextColor -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.ZapIcon import com.vitorpamplona.amethyst.ui.note.ZappedIcon import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt index e27bdaed5c..2ff50fbc24 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayUncitedHashtags.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.produceState import com.vitorpamplona.amethyst.commons.richtext.HashTagSegment import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.ui.components.ClickableTextColor -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.HalfTopPadding import com.vitorpamplona.amethyst.ui.theme.lessImportantLink diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index 5969cec19d..4fe375a24e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -46,8 +46,8 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.GenericLoadable -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.note.externalLinkForNote import com.vitorpamplona.amethyst.ui.note.types.EditState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index 04286f945f..aef75ef970 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -39,8 +39,8 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.appendLink -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt index a4b257f7ca..5da9dc90e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt @@ -67,10 +67,10 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.appendLink -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.CloseIcon import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt index bfb2c15f8c..da19175f11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayExternalId.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayGeoHashExternalId.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayGeoHashExternalId.kt index 2ac4320e4b..c8afa029e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayGeoHashExternalId.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayGeoHashExternalId.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayHashtagExternalId.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayHashtagExternalId.kt index 2b806eb813..824e380047 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayHashtagExternalId.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/DisplayHashtagExternalId.kt @@ -40,8 +40,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withLink import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 2990dcd044..d05efddcea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments import android.net.Uri import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -39,8 +38,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment.Companion.CenterVertically @@ -56,11 +53,10 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton -import com.vitorpamplona.amethyst.ui.navigation.Nav +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav +import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList @@ -85,7 +81,6 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size5dp -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier import kotlinx.collections.immutable.persistentListOf @@ -144,47 +139,25 @@ fun GenericCommentPostScreen( Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = CenterVertically, - ) { - Spacer(modifier = StdHorzSpacer) - PostButton( - onPost = { - // uses the accountViewModel scope to avoid cancelling this - // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendPostSync() - nav.popBack() - } - }, - isActive = postViewModel.canPost(), - ) + PostingTopBar( + isActive = postViewModel::canPost, + onCancel = { + // uses the accountViewModel scope to avoid cancelling this + // function when the postViewModel is released + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.sendDraftSync() + nav.popBack() + postViewModel.cancel() } }, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - // uses the accountViewModel scope to avoid cancelling this - // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendDraftSync() - nav.popBack() - postViewModel.cancel() - } - }, - ) + onPost = { + // uses the accountViewModel scope to avoid cancelling this + // function when the postViewModel is released + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.sendPostSync() + nav.popBack() } }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index 64533d9a88..f690866fdf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -61,7 +61,7 @@ import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LinkIcon import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index 9c36dc2e57..bfce94baff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -46,8 +46,8 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt index 2d90c83404..7b770f447c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Badge.kt @@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt index 7b2923217b..9709ac8906 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChannelMessage.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelHeader import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt index 12b858d905..d373f98eb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt @@ -33,8 +33,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.ChatroomHeader import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt index 06a262ab95..4e65ca63d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.unit.dp 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.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderEncryptedFile import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.ChatroomHeader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt index 0afc218885..b4e0ab5bf1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Classifieds.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 299d4cc0a6..996fa443b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -54,8 +54,8 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt index 4f8a0c237f..0f0ebbd65b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.ShowMoreButton -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index 5ab551357a..bf5aad6cb9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -46,7 +46,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index 67d96b491a..8a1936bbee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -49,8 +49,8 @@ import com.vitorpamplona.amethyst.ui.components.DisplayEvent import com.vitorpamplona.amethyst.ui.components.RenderUserAsClickableText import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.measureSpaceWidth -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt index 4da053d35f..cf124de8f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/InteractiveStory.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFind import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt index 4944db6f0e..90949d90cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivity.kt @@ -52,9 +52,9 @@ import com.vitorpamplona.amethyst.service.playback.composable.VideoView import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.MyAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.UsernameDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt index 100f091e8a..6526e77f4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LiveActivityChatMessage.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivitiesChannelHeader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt index bbeded0da7..235cdea0e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/LongForm.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt index 6cfb180a50..6ee786b6ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/MedicalData.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.model.Resource import com.vitorpamplona.amethyst.model.VisionPrescription import com.vitorpamplona.amethyst.model.findReferenceInDb import com.vitorpamplona.amethyst.model.parseResourceBundleOrNull -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt index 0ef07dc78f..66c9bb98ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt index 400a910d2d..f4ce7b4152 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.note.types import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt index a5209e249b..23503060d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ShowMoreButton -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt index 9f9eae7730..67ad354aa7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PictureDisplay.kt @@ -41,7 +41,7 @@ import com.vitorpamplona.amethyst.ui.components.AutoNonlazyGrid import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt index e9e341d3f3..39eafb0c38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PinList.kt @@ -48,7 +48,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.PinIcon import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt index 4cb79ba44a..089b67877b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Poll.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.PollNote import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index d82c20c54e..be3c353284 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -38,8 +38,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt index 813decab40..eb2a23085a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Reaction.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index 15d3b65508..efa085dddb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -47,8 +47,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserRelayIntoList import com.vitorpamplona.amethyst.ui.components.ShowMoreButton -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.AddRelayButton import com.vitorpamplona.amethyst.ui.note.RemoveRelayButton import com.vitorpamplona.amethyst.ui.note.getGradient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt index bf2e6e410f..3a3f336156 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RenderPostApproval.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt index ecb3011f5b..bbc44aab40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Report.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index baef7d2914..49867e15e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -38,7 +38,7 @@ 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.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt index 8724e3e3b0..7a75c42ffe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt @@ -50,8 +50,8 @@ import com.vitorpamplona.amethyst.ui.actions.EditPostView import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.NoteBody import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt index d8ebca3755..81a196ec63 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Torrent.kt @@ -53,8 +53,8 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.ui.components.ShowMoreButton -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt index 80332b0ced..d7d1fff853 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt @@ -50,9 +50,9 @@ import com.vitorpamplona.amethyst.service.countToHumanReadableBytes import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.LoadNote -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt index f9b7c9f2d0..0ae502b832 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Video.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt index b130ee3277..2a12a87985 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Wiki.kt @@ -39,7 +39,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.MyAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt index 87321b417c..7976be8ef3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountState.kt @@ -27,7 +27,7 @@ import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.Route sealed class AccountState { object Loading : AccountState() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 1faadedf2f..6cb102b28e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.amethyst.model.DefaultNIP65List import com.vitorpamplona.amethyst.model.DefaultNIP65RelaySet import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 75e104d750..beb4807fdb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyGridState import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt index aec8b10241..11203eddf6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.checkNotInMainThread -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt index 01ebfee6b2..a89651c332 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedView.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt index 6ab8b01b5f..e3e361ebf9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt @@ -37,9 +37,9 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalConfiguration -import com.vitorpamplona.amethyst.ui.navigation.AccountSwitchBottomSheet -import com.vitorpamplona.amethyst.ui.navigation.DrawerContent -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.drawer.AccountSwitchBottomSheet +import com.vitorpamplona.amethyst.ui.navigation.drawer.DrawerContent +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 18bbf3733b..61623711f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -71,8 +71,8 @@ import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager import com.vitorpamplona.amethyst.ui.feeds.FeedState -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.showAmount diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index 13ecb24c5b..d4b166713a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -47,7 +47,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.Account import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.AppNavigation -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt index 2ea557319d..dd013814e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt @@ -40,8 +40,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal.BookmarkPrivateFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt index 9961616620..b27f5ed3de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatFeedView.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index 4d917f9800..b92c80cf40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -44,9 +44,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.DisplayDraftChat import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.NoteQuickActionMenu diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt index 6d54512b8f..64352fb5e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/DrawAuthorInfo.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.FollowingIcon import com.vitorpamplona.amethyst.ui.note.InnerUserPicture import com.vitorpamplona.amethyst.ui.note.WatchUserFollows diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt index 817ca2b0f7..73753a70b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderChangeChannelMetadataNote.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt index fbfcdcbcfc..dd7f65e173 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderCreateChannelNote.kt @@ -37,7 +37,6 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -55,9 +54,9 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt index a8ddd36515..99a5d2031d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderDraftEvent.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.NoteRow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt index a708517904..188edfcaef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderEncryptedFile.kt @@ -37,7 +37,7 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt index 457c9e50e5..5bd75f486a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/types/RenderRegularTextNote.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt index 7e4af311da..e29488fd20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomByAuthorScreen.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RenderRoomTopBar import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt index fd8c922f83..79bb6ca192 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt @@ -31,7 +31,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RenderRoomTopBar import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index cf3e1f83c0..a5f198c9ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMs import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt index c51938ba6c..d412f75c12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RenderRoomTopBar.kt @@ -47,8 +47,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.UserCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index d2c440c7c1..6d78249197 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -50,8 +50,6 @@ import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -70,8 +68,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -92,12 +88,11 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.components.ZoomableContentView -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Nav -import com.vitorpamplona.amethyst.ui.navigation.routeToMessage +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage +import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.BaseUserPicture -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList @@ -122,8 +117,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font14SP -import com.vitorpamplona.amethyst.ui.theme.HalfEndPadding -import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size35dp @@ -177,39 +170,31 @@ fun NewGroupDMScreen( Scaffold( topBar = { - TopAppBar( - actions = { - ActionButton(postViewModel, accountViewModel, nav) + PostingTopBar( + titleRes = R.string.private_message, + isActive = postViewModel::canPost, + onCancel = { + // uses the accountViewModel scope to avoid cancelling this + // function when the postViewModel is released + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.sendDraftSync() + delay(100) + nav.popBack() + postViewModel.cancel() + } }, - title = { - Text( - text = stringRes(R.string.private_message), - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) + onPost = { + // uses the accountViewModel scope to avoid cancelling this + // function when the postViewModel is released + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.sendPostSync() + postViewModel.room?.let { + nav.nav(routeToMessage(it, null, null, null, accountViewModel)) + } + postViewModel.cancel() + } + nav.popBack() }, - navigationIcon = { - CloseButton( - modifier = HalfStartPadding, - onPress = { - // uses the accountViewModel scope to avoid cancelling this - // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendDraftSync() - delay(100) - nav.popBack() - postViewModel.cancel() - } - }, - ) - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> @@ -225,30 +210,6 @@ fun NewGroupDMScreen( } } -@Composable -fun ActionButton( - postViewModel: ChatNewMessageViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - PostButton( - modifier = HalfEndPadding, - onPost = { - // uses the accountViewModel scope to avoid cancelling this - // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendPostSync() - postViewModel.room?.let { - nav.nav(routeToMessage(it, null, null, null, accountViewModel)) - } - postViewModel.cancel() - } - nav.popBack() - }, - isActive = postViewModel.canPost(), - ) -} - @Composable fun GroupDMScreenContent( postViewModel: ChatNewMessageViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 19868a1b33..0d489578b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -38,8 +38,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt index 918cff6384..3a8c60fb5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploadDialog.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameOnlyDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index 4df7c2ffe0..381d21df8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt index 6287ebb2a5..a281a077f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/EphemeralChatScreen.kt @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.EphemeralChatTopBar import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt index fe3c58c4b7..3b24c1b669 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt @@ -27,8 +27,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.StdPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt index d66213010a..54973442ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephem import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt index 17f80f545a..9e92d4873f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.fasterxml.jackson.databind.util.ClassUtil.exceptionMessage import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.FeatureSetType @@ -45,7 +44,7 @@ import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.actions.JoinChatButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.actions.LeaveChatButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt index 5e3a2473eb..1bf4211e18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt @@ -25,7 +25,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt index 28eb28e6d5..c6119b2208 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt @@ -25,7 +25,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt index 2a8cb36583..e25b6bcbfd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/metadata/NewEphemeralChatScreen.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -36,31 +34,24 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.MinHorzSpacer import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -104,50 +95,18 @@ private fun ChannelMetadataScaffold( ) { Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = MinHorzSpacer) - - Text( - text = stringRes(R.string.relay_chat), - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - - JoinButton( - onPost = { - postViewModel.buildRoom()?.let { - nav.popBack() - nav.nav(routeFor(it)) - } - }, - postViewModel.canPost, - ) + PostingTopBar( + titleRes = R.string.relay_chat, + isActive = postViewModel::canPost, + onCancel = { + nav.popBack() + }, + onPost = { + postViewModel.buildRoom()?.let { + nav.popBack() + nav.nav(routeFor(it)) } }, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - postViewModel.clear() - nav.popBack() - }, - ) - } - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index bf720d9e50..36a0299877 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt index 28b935b0e2..c5bfd8a49d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelHeader.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatChannelHeader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt index 2e3f46e274..8c7cd6b93c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/PublicChatChannelScreen.kt @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.PublicChatTopBar diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt index 0ddf9f3d8e..24a022fe00 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -49,7 +49,7 @@ import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt index 17df36cc19..05d43fd87e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.StdPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt index bbca7b56e0..842d0872f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28 import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt index 45bc9c57eb..e5d265ffa0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observe import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt index 8a7ba1bcc3..a2d1e9b2ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ZeroPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt index 8dacd02330..7ee041c53d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt @@ -25,7 +25,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt index 9f6b44c700..54e10431c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt @@ -25,7 +25,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ButtonPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt index 4229887983..856c01edb1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt index 1108eb925f..d94179393a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt index ae533e4e00..44a11f3781 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.PublicChatChannel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.njumpLink import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index db551defb1..b923e5812c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -37,17 +35,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -55,12 +48,12 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory @@ -69,10 +62,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditF import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.MinHorzSpacer import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -87,9 +78,7 @@ fun ChannelMetadataScreen( ChannelMetadataScreen(null as PublicChatChannel?, accountViewModel, nav) } else { LoadPublicChatChannel(channelId, accountViewModel) { - if (it is PublicChatChannel) { - ChannelMetadataScreen(it, accountViewModel, nav) - } + ChannelMetadataScreen(it, accountViewModel, nav) } } } @@ -135,61 +124,35 @@ private fun ChannelMetadataScaffold( ) { Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = MinHorzSpacer) - - Text( - text = stringRes(R.string.public_chat), - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - - if (postViewModel.isNewChannel()) { - CreateButton( - onPost = { - postViewModel.createOrUpdate { - nav.nav(routeFor(it)) - } - nav.popBack() - }, - postViewModel.canPost, - ) - } else { - SaveButton( - onPost = { - postViewModel.createOrUpdate { } - nav.popBack() - }, - postViewModel.canPost, - ) + if (postViewModel.isNewChannel()) { + CreatingTopBar( + titleRes = R.string.public_chat, + isActive = postViewModel::canPost, + onCancel = { + postViewModel.clear() + nav.popBack() + }, + onPost = { + postViewModel.createOrUpdate { + nav.nav(routeFor(it)) } - } - }, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - postViewModel.clear() - nav.popBack() - }, - ) - } - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), - ) + nav.popBack() + }, + ) + } else { + SavingTopBar( + titleRes = R.string.public_chat, + isActive = postViewModel::canPost, + onCancel = { + postViewModel.clear() + nav.popBack() + }, + onPost = { + postViewModel.createOrUpdate { } + nav.popBack() + }, + ) + } }, ) { pad -> val feedState by postViewModel.channelRelays.collectAsStateWithLifecycle() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt index 4b6471beb4..e3a32fa12a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt index a6c5623817..7ff1f2be66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.StdPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt index f686c5c946..4136ed19d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivityChannelScreen.kt @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivityTopBar diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt index 7189bd49ba..04e2be6165 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt @@ -45,8 +45,8 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt index 699fdb6b5a..4ff5e91941 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt @@ -40,7 +40,7 @@ import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.ZapReaction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt index 475f8301ae..d9ae207d4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53 import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LongLiveActivityChannelHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ShortLiveActivityChannelHeader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index 7d41e7a285..b7313cdbd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index 87497d68d2..c2f20ad7ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -39,7 +39,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt index a925625596..a2913fd3c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChannelFabColumn.kt @@ -48,9 +48,9 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.Route.NewGroupDM +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.Route.NewGroupDM import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font12SP import com.vitorpamplona.amethyst.ui.theme.Size55Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 7f0f26e048..7207192f72 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -61,9 +61,9 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.layouts.ChatHeaderLayout -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.BlankNote import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContentOrNull import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MessagesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MessagesScreen.kt index d9717c0ade..000ab65f9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MessagesScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/MessagesScreen.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.singlepane.MessagesSinglePane import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane.MessagesTwoPane diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index 93b19a14c7..2901cd5054 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -41,7 +41,7 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChatroomHeaderCompose import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index 8eb631bd2f..95a77e328b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -49,7 +49,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.feeds.FeedContentState -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size40dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt index 4b9038fa88..28d5be2c30 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt @@ -32,10 +32,11 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.MainTopBar -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.topbars.AmethystClickableIcon +import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelFabColumn import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription @@ -74,7 +75,7 @@ fun MessagesSinglePane( isInvertedLayout = false, topBar = { Column { - MainTopBar(accountViewModel, nav) + UserDrawerSearchTopBar(accountViewModel, nav) { AmethystClickableIcon() } MessagesTabHeader( pagerState, tabs, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt index 0861aeee49..713a6f0716 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt @@ -33,7 +33,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.feed.MessagesPager diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 46e48e4011..56c1e1e331 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -37,10 +36,11 @@ import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy import com.google.accompanist.adaptive.TwoPane import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.MainTopBar -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.topbars.AmethystClickableIcon +import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.Chatroom import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelView @@ -71,9 +71,7 @@ fun MessagesTwoPane( DisappearingScaffold( isInvertedLayout = false, topBar = { - Column { - MainTopBar(accountViewModel, nav) - } + UserDrawerSearchTopBar(accountViewModel, nav) { AmethystClickableIcon() } }, bottomBar = { AppBottomBar(Route.Message, accountViewModel) { route -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt index 693c2ff3b7..214a74fc59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/TwoPaneNav.kt @@ -22,16 +22,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.twopane import androidx.compose.material3.DrawerState import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.reflect.KClass class TwoPaneNav( val nav: INav, - val scope: CoroutineScope, + override val scope: CoroutineScope, ) : INav { override val drawerState: DrawerState = nav.drawerState @@ -46,7 +45,7 @@ class TwoPaneNav( } override fun nav(computeRoute: suspend () -> Route?) { - scope.launch(Dispatchers.Default) { + scope.launch { val route = computeRoute() if (route != null) { if (route is Route.Room || route is Route.PublicChatChannel) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt index 87ac292f1f..668878e1d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadDialog.kt @@ -41,7 +41,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -59,9 +58,9 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.ShowImageUploadGallery import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TitleIconModifier -import com.vitorpamplona.amethyst.ui.navigation.rememberHeightDecreaser +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar +import com.vitorpamplona.amethyst.ui.navigation.topbars.TitleIconModifier import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.SettingSwitchItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -97,9 +96,7 @@ fun ChatFileUploadDialog( SetDialogToEdgeToEdge() Scaffold( topBar = { - TopAppBar( - scrollBehavior = rememberHeightDecreaser(), - modifier = Modifier, + ShorterTopAppBar( title = title, navigationIcon = { IconButton( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt index 99ea67eb66..cecbca8e02 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/DisplayReplyingToNote.kt @@ -37,7 +37,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.ChatroomMessageCompose import com.vitorpamplona.amethyst.ui.theme.Size20Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt index 665c55c89a..f36fbf65cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/CommunityScreen.kt @@ -32,8 +32,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.types.LongCommunityHeader import com.vitorpamplona.amethyst.ui.note.types.ShortCommunityHeader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt index 67ea494ab7..158eab186c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/NewCommunityNoteButton.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.LoadNote -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt index 72b9beb24d..c9ed3da56b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.SensitivityWarning -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.ClickableNote import com.vitorpamplona.amethyst.ui.note.LongPressToQuickAction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index b037e6d3b3..30d7da9c10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -69,9 +69,9 @@ import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.TabItem diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoveryTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoveryTopBar.kt index 18fab1ffbe..2101fe7b6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoveryTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoveryTopBar.kt @@ -23,26 +23,47 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.FollowListWithoutRoutes -import com.vitorpamplona.amethyst.ui.navigation.GenericMainTopBar -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner +import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar +import com.vitorpamplona.amethyst.ui.screen.FeedDefinition +import com.vitorpamplona.amethyst.ui.screen.FollowListState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes @Composable fun DiscoveryTopBar( accountViewModel: AccountViewModel, nav: INav, ) { - GenericMainTopBar(accountViewModel, nav) { + UserDrawerSearchTopBar(accountViewModel, nav) { val list by accountViewModel.account.settings.defaultDiscoveryFollowList .collectAsStateWithLifecycle() - FollowListWithoutRoutes( + FollowList( followListsModel = accountViewModel.feedStates.feedListOptions, listName = list, - accountViewModel, - ) { listName -> - accountViewModel.account.settings.changeDefaultDiscoveryFollowList(listName.code) - } + accountViewModel = accountViewModel, + onChange = accountViewModel.account.settings::changeDefaultDiscoveryFollowList, + ) } } + +@Composable +private fun FollowList( + followListsModel: FollowListState, + listName: String, + accountViewModel: AccountViewModel, + onChange: (FeedDefinition) -> Unit, +) { + val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle() + + FeedFilterSpinner( + placeholderCode = listName, + explainer = stringRes(R.string.select_list_to_filter), + options = allLists, + onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow) }, + accountViewModel = accountViewModel, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt index 3661fb5f49..317275f2e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormCard.kt @@ -24,7 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.types.LongFormHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index d1e4794050..44e0a3bd8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCo import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.note.LikeReaction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt index e26262a4cd..f186b54fa8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt @@ -43,8 +43,8 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.components.MyAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.UserPicture diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index b31d46c232..7391c8c784 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCo import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt index 464b935122..9be21bae56 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCo import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.note.LikeReaction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt index cbf68e2dd0..34a00ce158 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DVMCard.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs -import android.R.attr.author import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -47,8 +46,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LikeReaction import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.elements.BannerImage @@ -62,7 +60,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.nip05 -import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError @Immutable data class DVMCard( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt index ee84f3d0a0..df2a99c3a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/ClassifiedsThumb.kt @@ -46,7 +46,7 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.DisplayAuthorBanner import com.vitorpamplona.amethyst.ui.note.showAmountInteger import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index fe301d25c6..9bb7e67be8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -22,10 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds import android.net.Uri import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -39,8 +37,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment.Companion.CenterVertically @@ -56,11 +52,10 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Nav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav +import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.BaseUserPicture -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList @@ -84,7 +79,6 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size5dp -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -143,45 +137,23 @@ fun NewProductScreen( Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = CenterVertically, - ) { - Spacer(modifier = StdHorzSpacer) - - PostButton( - onPost = { - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendPostSync() - nav.popBack() - postViewModel.cancel() - } - }, - isActive = postViewModel.canPost(), - ) + PostingTopBar( + titleRes = R.string.new_product, + isActive = postViewModel::canPost, + onCancel = { + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.sendDraftSync() + nav.popBack() + postViewModel.cancel() } }, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendDraftSync() - nav.popBack() - postViewModel.cancel() - } - }, - ) + onPost = { + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.sendPostSync() + nav.popBack() + postViewModel.cancel() } }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index bfda8be7e2..ad48a8edb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -57,8 +57,8 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index b9509a2dd4..85ca853cd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -70,7 +70,7 @@ import com.vitorpamplona.amethyst.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt index f37cfa9705..d310456396 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmTopBar.kt @@ -28,8 +28,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.MyAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt index 368e021d60..6baf3fdf32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashPostScreen.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.navigation.Nav +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt index aff0303d24..03877e6942 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -33,9 +33,9 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingGeohash import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt index 7a79446286..9191b0839f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/NewGeoNoteButton.kt @@ -27,8 +27,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt index 27b70f85a0..bf5fb2db16 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt @@ -24,7 +24,7 @@ import android.annotation.SuppressLint import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @SuppressLint("StateFlowValueCalledInComposition") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt index d96a6a847b..59d4e8f8f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagPostScreen.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia -import com.vitorpamplona.amethyst.ui.navigation.Nav +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.note.nip22Comments.CommentPostViewModel import com.vitorpamplona.amethyst.ui.note.nip22Comments.GenericCommentPostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt index 7a87339180..409b7246c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt @@ -38,9 +38,9 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingHashtag import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.dal.HashtagFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt index 77ea685235..287971b8a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/NewHashtagNoteButton.kt @@ -27,8 +27,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt index 51c1c20481..4f36e302d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt @@ -20,11 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource -import android.R.attr.tag import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 65e81afb7e..c3cd55dedf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -71,9 +71,9 @@ import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.NewGeoPostButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeTopBar.kt index fd522e3971..c61f41794a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeTopBar.kt @@ -23,21 +23,25 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.FollowListWithRoutes -import com.vitorpamplona.amethyst.ui.navigation.GenericMainTopBar -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner +import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar +import com.vitorpamplona.amethyst.ui.screen.FeedDefinition +import com.vitorpamplona.amethyst.ui.screen.FollowListState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes @Composable fun HomeTopBar( accountViewModel: AccountViewModel, nav: INav, ) { - GenericMainTopBar(accountViewModel, nav) { + UserDrawerSearchTopBar(accountViewModel, nav) { val list by accountViewModel.account.settings.defaultHomeFollowList .collectAsStateWithLifecycle() - FollowListWithRoutes( + FollowList( followListsModel = accountViewModel.feedStates.feedListOptions, listName = list, accountViewModel = accountViewModel, @@ -50,3 +54,21 @@ fun HomeTopBar( } } } + +@Composable +private fun FollowList( + followListsModel: FollowListState, + listName: String, + accountViewModel: AccountViewModel, + onChange: (FeedDefinition) -> Unit, +) { + val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle() + + FeedFilterSpinner( + placeholderCode = listName, + explainer = stringRes(R.string.select_list_to_filter), + options = allLists, + onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow) }, + accountViewModel = accountViewModel, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt index 4478b6cf94..cc87c14c6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/NewNoteButton.kt @@ -27,8 +27,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size26Modifier diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 2ebc002103..eb67689f16 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -24,7 +24,6 @@ import android.content.Intent import android.net.Uri import android.os.Parcelable import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -44,8 +43,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Switch -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -64,11 +61,10 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.components.getActivity -import com.vitorpamplona.amethyst.ui.navigation.Nav +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav +import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.PostButton import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList @@ -97,7 +93,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size5dp -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier import kotlinx.collections.immutable.persistentListOf @@ -173,49 +168,26 @@ private fun NewPostScreenInner( Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = CenterVertically, - ) { - Spacer(modifier = StdHorzSpacer) - - PostButton( - onPost = { - // uses the accountViewModel scope to avoid cancelling this - // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendPostSync() - delay(100) - nav.popBack() - } - }, - isActive = postViewModel.canPost(), - ) + PostingTopBar( + isActive = postViewModel::canPost, + onPost = { + // uses the accountViewModel scope to avoid cancelling this + // function when the postViewModel is released + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.sendPostSync() + delay(100) + nav.popBack() } }, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - // uses the accountViewModel scope to avoid cancelling this - // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendDraftSync() - nav.popBack() - postViewModel.cancel() - } - }, - ) + onCancel = { + // uses the accountViewModel scope to avoid cancelling this + // function when the postViewModel is released + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.sendDraftSync() + nav.popBack() + postViewModel.cancel() } }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt index 7e1e37e4e0..f722af5088 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelNoteAuthors -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.Gallery import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt index 719dd6063c..45f673adf0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt @@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.feeds.rememberForeverLazyListState -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.BadgeCompose import com.vitorpamplona.amethyst.ui.note.MessageSetCompose import com.vitorpamplona.amethyst.ui.note.MultiSetCompose diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt index 6cf2d40c1a..2127ba8252 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationScreen.kt @@ -38,9 +38,9 @@ import com.google.accompanist.permissions.rememberPermissionState import com.vitorpamplona.amethyst.ui.components.SelectNotificationProvider import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationTopBar.kt index 2160678dcf..4724b827fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationTopBar.kt @@ -23,26 +23,47 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.FollowListWithoutRoutes -import com.vitorpamplona.amethyst.ui.navigation.GenericMainTopBar -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner +import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar +import com.vitorpamplona.amethyst.ui.screen.FeedDefinition +import com.vitorpamplona.amethyst.ui.screen.FollowListState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes @Composable fun NotificationTopBar( accountViewModel: AccountViewModel, nav: INav, ) { - GenericMainTopBar(accountViewModel, nav) { + UserDrawerSearchTopBar(accountViewModel, nav) { val list by accountViewModel.account.settings.defaultNotificationFollowList .collectAsStateWithLifecycle() - FollowListWithoutRoutes( + FollowLists( followListsModel = accountViewModel.feedStates.feedListOptions, listName = list, - accountViewModel, - ) { listName -> - accountViewModel.account.settings.changeDefaultNotificationFollowList(listName.code) - } + accountViewModel = accountViewModel, + onChange = accountViewModel.account.settings::changeDefaultNotificationFollowList, + ) } } + +@Composable +private fun FollowLists( + followListsModel: FollowListState, + listName: String, + accountViewModel: AccountViewModel, + onChange: (FeedDefinition) -> Unit, +) { + val allLists by followListsModel.kind3GlobalPeople.collectAsStateWithLifecycle() + + FeedFilterSpinner( + placeholderCode = listName, + explainer = stringRes(R.string.select_list_to_filter), + options = allLists, + onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow) }, + accountViewModel = accountViewModel, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt index cae2914e39..87179f8f4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt @@ -20,34 +20,22 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.tor.PrivacySettingsBody import com.vitorpamplona.amethyst.ui.tor.TorDialogViewModel @@ -68,41 +56,15 @@ fun PrivacyOptionsScreen( Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = StdHorzSpacer) - - Text( - stringRes(id = R.string.privacy_options), - ) - SaveButton( - onPost = { - accountViewModel.setTorSettings(dialogViewModel.save()) - nav.popBack() - }, - isActive = true, - ) - } + SavingTopBar( + titleRes = R.string.privacy_options, + onCancel = { + nav.popBack() }, - navigationIcon = { - Row { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - nav.popBack() - }, - ) - } + onPost = { + accountViewModel.setTorSettings(dialogViewModel.save()) + nav.popBack() }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index c064f5c02d..f62ec2ac3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -63,7 +63,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.BookmarkTabHeader import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.TabBookmarks diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/TabBookmarks.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/TabBookmarks.kt index 63163d4b22..73f09154d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/TabBookmarks.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/TabBookmarks.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal.UserProfileBookmarksFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/TabNotesConversations.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/TabNotesConversations.kt index 9ffe26481b..e5c89e3c03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/TabNotesConversations.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/TabNotesConversations.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.dal.UserProfileConversationsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt index 4349f3f10e..d242b96256 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollowers -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt index 0e42b45a14..aaa112059d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserFollows -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt index c02a5be3a9..be54c2dade 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt @@ -30,8 +30,8 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.components.SensitivityWarning -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt index ff2b851003..6c68988524 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryThumb.kt @@ -60,7 +60,7 @@ import com.vitorpamplona.amethyst.ui.components.DisplayBlurHash import com.vitorpamplona.amethyst.ui.components.ImageUrlWithDownloadButton import com.vitorpamplona.amethyst.ui.components.LoadingAnimation import com.vitorpamplona.amethyst.ui.components.SensitivityWarning -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon import com.vitorpamplona.amethyst.ui.note.WatchAuthor import com.vitorpamplona.amethyst.ui.note.elements.BannerImage diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt index adddcb61c9..c4f8729cbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/ProfileGalleryFeed.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.FeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.FeedPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/TabGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/TabGallery.kt index 0e57acbe95..c1f49d05ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/TabGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/TabGallery.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.dal.UserProfileGalleryFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt index ee5cae3bc3..718d2b814e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt @@ -33,8 +33,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserTagFollows -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagHeader import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt index 07a8930157..68221abeec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt @@ -35,8 +35,8 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeToMessage +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequestCard diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index b1b3b81d81..59fb554333 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -52,7 +52,7 @@ import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.DrawPlayName import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditButton.kt index c4db3cba31..9a1a41d06a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditButton.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.ZeroPadding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt index 9f161ddc8c..9c747a6125 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt index 1601f63e3e..92f7c6d737 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt @@ -26,7 +26,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt index cc2a1809b8..0ef030fde9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt @@ -51,7 +51,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt index 30d0abb84e..fc6b2359b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt @@ -36,7 +36,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.feeds.FeedState -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt index 3830032eda..2ead89f33f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.draw.clip import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt index 4c94c02e7f..47d054a68c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt @@ -50,8 +50,8 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/TabMutualConversations.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/TabMutualConversations.kt index 92f1f085cb..9eef64867c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/TabMutualConversations.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/TabMutualConversations.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProfileMutualFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt index d4897240a7..f799c97503 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt @@ -24,7 +24,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt index d83373ab02..af1206d858 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt @@ -31,9 +31,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.RelayInfo import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.RelayCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt index 5e5c4864c2..2a2a4258af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt @@ -30,7 +30,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt index e487ff0112..8701d7e946 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.dal.UserProfileReportFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt index 9807d23654..acd98d7cb1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.ZapNoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt index 6fe532ab11..e51280ff2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt @@ -25,7 +25,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.dal.UserProfileZapsFeedViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt index 30a3e7ee5a..3505d8b848 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/QrCodeScanner.kt @@ -28,7 +28,7 @@ import com.google.zxing.client.android.Intents import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanOptions import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.uriToRoute diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt index 8c33d63e09..98d79371ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/qrcode/ShowQRDialog.kt @@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.ui.components.DisplayNIP05 import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt index 87dc2c266a..2e2ea23472 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -37,10 +36,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.components.LoadNote -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Nav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 4afe17001a..afe578239f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -33,23 +32,18 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.DefaultDMRelayList import com.vitorpamplona.amethyst.model.DefaultSearchRelayList -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems @@ -71,7 +65,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.TrustedRelay import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.renderTrustedItems import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier @@ -151,52 +144,28 @@ fun MappedAllRelayListView( Scaffold( topBar = { - TopAppBar( - title = { - Text( - text = stringRes(R.string.relay_settings), - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) + SavingTopBar( + titleRes = R.string.relay_settings, + onCancel = { + dmViewModel.clear() + nip65ViewModel.clear() + searchViewModel.clear() + localViewModel.clear() + privateOutboxViewModel.clear() + trustedViewModel.clear() + blockedViewModel.clear() + newNav.popBack() }, - navigationIcon = { - CloseButton( - modifier = HalfHorzPadding, - onPress = { - dmViewModel.clear() - nip65ViewModel.clear() - searchViewModel.clear() - localViewModel.clear() - privateOutboxViewModel.clear() - trustedViewModel.clear() - blockedViewModel.clear() - newNav.popBack() - }, - ) + onPost = { + dmViewModel.create() + nip65ViewModel.create() + searchViewModel.create() + localViewModel.create() + privateOutboxViewModel.create() + trustedViewModel.create() + blockedViewModel.create() + newNav.popBack() }, - actions = { - SaveButton( - modifier = HalfHorzPadding, - isActive = true, - onPost = { - dmViewModel.create() - nip65ViewModel.create() - searchViewModel.create() - localViewModel.create() - privateOutboxViewModel.create() - trustedViewModel.create() - blockedViewModel.create() - newNav.popBack() - }, - ) - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index d5578ac6d1..708e6c4db5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.ClickableEmail import com.vitorpamplona.amethyst.ui.components.ClickableUrl import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.timeAgo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt index 05169ebab5..3e3c1eaf58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListView.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 8da93ea6c7..e5b2b7b4aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt index 568d46f08d..5b27d5890d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListView.kt @@ -28,8 +28,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt index 74240d809f..8c7bc3cec4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt @@ -22,9 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api @@ -32,11 +30,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -45,13 +40,11 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.DefaultDMRelayList import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.imageModifier @@ -77,39 +70,16 @@ fun AddDMRelayListDialog( SetDialogToEdgeToEdge() Scaffold( topBar = { - TopAppBar( - title = { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Spacer(modifier = StdHorzSpacer) - - Text(stringRes(R.string.dm_relays_title)) - - SaveButton( - onPost = { - postViewModel.create() - onClose() - }, - true, - ) - } + SavingTopBar( + titleRes = R.string.dm_relays_title, + onCancel = { + postViewModel.clear() + onClose() }, - navigationIcon = { - Spacer(modifier = StdHorzSpacer) - CloseButton( - onPress = { - postViewModel.clear() - onClose() - }, - ) + onPost = { + postViewModel.create() + onClose() }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt index 50c4fbf22f..79e66ae290 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt index e50b13994e..01a9fe26d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListView.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt index ed4be6a3e4..e447b5a0ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt index 829b36153e..12be77dfe3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt index 495daa68d0..139e5344c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt index 1b9d872f88..355506f36b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListView.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt index f2b92ebf21..a8ea3ea388 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt index a5a0cea80e..5735340253 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.search import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api @@ -31,13 +30,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties @@ -45,15 +40,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayList import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayListViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.imageModifier @@ -79,40 +72,16 @@ fun AddSearchRelayListDialog( SetDialogToEdgeToEdge() Scaffold( topBar = { - TopAppBar( - navigationIcon = { - CloseButton( - modifier = HalfHorzPadding, - onPress = { - postViewModel.clear() - onClose() - }, - ) + SavingTopBar( + titleRes = R.string.search_relays_title, + onCancel = { + postViewModel.clear() + onClose() }, - title = { - Text( - text = stringRes(R.string.search_relays_title), - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleLarge, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) + onPost = { + postViewModel.create() + onClose() }, - actions = { - SaveButton( - modifier = HalfHorzPadding, - isActive = true, - onPost = { - postViewModel.create() - onClose() - }, - ) - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface, - ), ) }, ) { pad -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 99d8ca6009..65f4d52e0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -60,10 +60,10 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relayClient.searchCommand.TextSearchDataSourceSubscription import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClearTextIcon import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.SearchIcon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 5eb38c0aaf..64523f48d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -57,8 +57,8 @@ import com.vitorpamplona.amethyst.model.parseGalleryType import com.vitorpamplona.amethyst.model.parseThemeType import com.vitorpamplona.amethyst.ui.components.PushNotificationSettingsRow import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt index 3b821ffe74..116fc3e298 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt @@ -23,22 +23,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.rememberHeightDecreaser -import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountContent import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountViewModel -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes @Composable fun NIP47SetupScreen( @@ -61,28 +55,16 @@ fun NIP47SetupScreen( ) { Scaffold( topBar = { - TopAppBar( - scrollBehavior = rememberHeightDecreaser(), - title = { Text(stringRes(id = R.string.wallet_connect)) }, - navigationIcon = { - IconButton( - onClick = { - postViewModel.cancel() - nav.popBack() - }, - modifier = Modifier, - ) { - ArrowBackIcon() - } + SavingTopBar( + titleRes = R.string.wallet_connect, + isActive = postViewModel::hasChanged, + onCancel = { + postViewModel.cancel() + nav.popBack() }, - actions = { - SaveButton( - onPost = { - postViewModel.sendPost() - nav.popBack() - }, - isActive = postViewModel.hasChanged(), - ) + onPost = { + postViewModel.sendPost() + nav.popBack() }, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index cb49a840a6..1a3f2f70eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -70,8 +70,8 @@ import com.vitorpamplona.amethyst.model.parseWarningType import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenWord import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.note.elements.AddButton import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt index 9f57b1af5d..b8603ca60c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -49,9 +49,9 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 107de49eef..52e91ab634 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -83,10 +83,10 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.ZoomableContentView import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox -import com.vitorpamplona.amethyst.ui.navigation.EmptyNav -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.routeFor -import com.vitorpamplona.amethyst.ui.navigation.routeToMessage +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.note.CheckAndDisplayEditStatus import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.DisplayDraft diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt index 5ba92fc7af..4ee2396a12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadScreen.kt @@ -31,8 +31,8 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFind import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.datasources.ThreadFilterAssemblerSubscription diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt index e04d5dd9aa..c5bb56067d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewImageButton.kt @@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewMediaView import com.vitorpamplona.amethyst.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePicture -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt index 9a8b7f8943..25f33b6006 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/StoriesTopBar.kt @@ -23,21 +23,25 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.ui.navigation.FollowListWithRoutes -import com.vitorpamplona.amethyst.ui.navigation.GenericMainTopBar -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner +import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar +import com.vitorpamplona.amethyst.ui.screen.FeedDefinition +import com.vitorpamplona.amethyst.ui.screen.FollowListState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes @Composable fun StoriesTopBar( accountViewModel: AccountViewModel, nav: INav, ) { - GenericMainTopBar(accountViewModel, nav) { + UserDrawerSearchTopBar(accountViewModel, nav) { val list by accountViewModel.account.settings.defaultStoriesFollowList .collectAsStateWithLifecycle() - FollowListWithRoutes( + FollowList( followListsModel = accountViewModel.feedStates.feedListOptions, listName = list, accountViewModel = accountViewModel, @@ -46,3 +50,21 @@ fun StoriesTopBar( } } } + +@Composable +private fun FollowList( + followListsModel: FollowListState, + listName: String, + accountViewModel: AccountViewModel, + onChange: (FeedDefinition) -> Unit, +) { + val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle() + + FeedFilterSpinner( + placeholderCode = listName, + explainer = stringRes(R.string.select_list_to_filter), + options = allLists, + onSelect = { onChange(allLists.getOrNull(it) ?: followListsModel.kind3Follow) }, + accountViewModel = accountViewModel, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index a699dfc1d0..9609d9a98d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -67,10 +67,10 @@ import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.ui.feeds.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold -import com.vitorpamplona.amethyst.ui.navigation.AppBottomBar -import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeFor +import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.BoostReaction import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.LikeReaction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 1eaa1d6b38..d5bbcb764d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -86,8 +86,6 @@ val Height100Modifier = Modifier.height(100.dp) val HalfDoubleVertSpacer = Modifier.height(7.dp) -val TopBarSize = 50.dp - 64.dp - val Size0dp = 0.dp val Size5dp = 5.dp val Size6dp = 6.dp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt index d87c012a48..fc370db073 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/tor/TorSettingsDialog.kt @@ -23,22 +23,19 @@ package com.vitorpamplona.amethyst.ui.tor import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface +import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType @@ -48,8 +45,8 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton -import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton +import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge +import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.TitleExplainer import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes @@ -74,14 +71,13 @@ fun ConnectTorDialog( decorFitsSystemWindows = false, ), ) { - Surface { - TorDialogContents( - torSettings, - onClose, - onPost, - onError, - ) - } + SetDialogToEdgeToEdge() + TorDialogContents( + torSettings, + onClose, + onPost, + onError, + ) } } @@ -121,24 +117,12 @@ fun TorDialogContents( onPost: (torSettings: TorSettings) -> Unit, onError: (String) -> Unit, ) { - Column( - modifier = - Modifier - .fillMaxSize() - .verticalScroll( - rememberScrollState(), - ).padding(10.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), - ) { - CloseButton(onPress = { onClose() }) - + Scaffold( + topBar = { val toastMessage = stringRes(R.string.invalid_port_number) - - SaveButton( + SavingTopBar( + titleRes = R.string.privacy_options, + onCancel = onClose, onPost = { try { onPost(dialogViewModel.save()) @@ -147,11 +131,19 @@ fun TorDialogContents( onError(toastMessage) } }, - isActive = true, ) + }, + ) { + Column( + Modifier + .padding(it) + .fillMaxSize() + .verticalScroll( + rememberScrollState(), + ).padding(horizontal = 10.dp), + ) { + PrivacySettingsBody(dialogViewModel) } - - PrivacySettingsBody(dialogViewModel) } } diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 4592190bb4..8d43e8b211 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -56,7 +56,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.lang.TranslationsCache import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness From 02a9563de67f3ebc17c83bea69e5c01428573d41 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 11 Jul 2025 14:33:43 -0400 Subject: [PATCH 089/185] Updates spotless --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f182838a6..b33c950c25 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,7 +41,7 @@ runner = "1.6.2" rfc3986 = "0.1.2" secp256k1KmpJniAndroid = "0.18.0" securityCryptoKtx = "1.1.0-beta01" -spotless = "7.0.4" +spotless = "7.1.0" torAndroid = "0.4.8.17" translate = "17.0.3" unifiedpush = "3.0.10" From bb5ae5d09e4bcebb280c5154e1de5ca4bf536563 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 11 Jul 2025 14:55:13 -0400 Subject: [PATCH 090/185] Fixes F-droid compilation due to a duplicated class from a unifiedpush crypto bug --- amethyst/build.gradle | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 99f896e128..bb29832c2a 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -171,6 +171,17 @@ android { } } +// TODO: until google merges and unifiedpush updates https://github.com/tink-crypto/tink-java-apps/pull/5 +configurations.all { + def tink = "com.google.crypto.tink:tink-android:1.17.0" + resolutionStrategy { + force(tink) + dependencySubstitution { + substitute module('com.google.crypto.tink:tink') using module(tink) + } + } +} + kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 From f93b08a272b6a23c745109b32bd55fff2e0856fd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 11 Jul 2025 17:10:51 -0400 Subject: [PATCH 091/185] Refactors master serializer name from EventMapper to JsonMapper --- .../vitorpamplona/amethyst/OkHttpOtsTest.kt | 8 +-- .../ThreadDualAxisChartAssemblerTest.kt | 4 +- .../amethyst/LocalPreferences.kt | 54 +++++++++---------- .../nip78AppSpecific/AppSpecificState.kt | 8 +-- .../quartz/benchmark/CacheBenchmark.kt | 4 +- .../quartz/benchmark/EventBenchmark.kt | 20 +++---- .../benchmark/EventCmdHasherBenchmark.kt | 4 +- .../quartz/benchmark/LargeCacheBenchmark.kt | 4 +- .../quartz/LargeDBSignatureCheck.kt | 6 +-- .../quartz/nip01Core/EventSigCheck.kt | 6 +-- .../quartz/nip57Zaps/PrivateZapTests.kt | 4 +- .../audio/header/tags/WaveformTag.kt | 6 +-- .../edits/PrivateOutboxRelayListEvent.kt | 6 +-- .../quartz/nip01Core/core/Event.kt | 9 ++-- .../quartz/nip01Core/crypto/EventHasher.kt | 4 +- .../jackson/EventManualSerializer.kt | 2 +- .../jackson/{EventMapper.kt => JsonMapper.kt} | 4 +- .../nip01Core/metadata/MetadataEvent.kt | 4 +- .../relay/commands/toClient/EventMessage.kt | 4 +- .../relay/commands/toClient/ToClientParser.kt | 4 +- .../relay/commands/toRelay/AuthCmd.kt | 4 +- .../relay/commands/toRelay/CountCmd.kt | 4 +- .../relay/commands/toRelay/EventCmd.kt | 4 +- .../relay/commands/toRelay/ReqCmd.kt | 4 +- .../relay/commands/toRelay/ToRelayParser.kt | 4 +- .../relay/filters/FilterSerializer.kt | 4 +- .../quartz/nip01Core/signers/EventTemplate.kt | 6 +-- .../quartz/nip02FollowList/RelaySet.kt | 6 +-- .../quartz/nip05DnsIdentifiers/Nip05.kt | 4 +- .../nip28PublicChat/base/ChannelData.kt | 6 +-- .../quartz/nip44Encryption/Nip44.kt | 4 +- .../BunkerResponseGetRelays.kt | 6 +-- .../nip46RemoteSigner/NostrConnectEvent.kt | 6 +-- .../LnZapPaymentRequestEvent.kt | 6 +-- .../LnZapPaymentResponseEvent.kt | 4 +- .../quartz/nip51Lists/GeneralListEvent.kt | 4 +- .../encryption/PrivateTagsInContent.kt | 6 +-- .../quartz/nip59Giftwrap/rumors/Rumor.kt | 6 +-- .../definition/AppMetadata.kt | 6 +-- .../NIP90ContentDiscoveryResponseEvent.kt | 4 +- .../nip01Core/metadata/UpdateMetadataTest.kt | 4 +- .../nip46RemoteSigner/BunkerRequestTest.kt | 4 +- 42 files changed, 135 insertions(+), 136 deletions(-) rename quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/{EventMapper.kt => JsonMapper.kt} (98%) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt index 0e38d73796..a2178a5061 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.ots.OkHttpBitcoinExplorer import com.vitorpamplona.amethyst.service.ots.OkHttpCalendarBuilder import com.vitorpamplona.amethyst.service.ots.OtsBlockHeightCache import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver @@ -58,21 +58,21 @@ class OkHttpOtsTest { @Test fun verifyNostrEvent() { - val ots = EventMapper.fromJson(otsEvent) as OtsEvent + val ots = JsonMapper.fromJson(otsEvent) as OtsEvent println(resolver.info(ots.otsByteArray())) assertEquals(1707688818L, ots.verify(resolver)) } @Test fun verifyNostrEvent2() { - val ots = EventMapper.fromJson(otsEvent2) as OtsEvent + val ots = JsonMapper.fromJson(otsEvent2) as OtsEvent println(resolver.info(ots.otsByteArray())) assertEquals(1706322179L, ots.verify(resolver)) } @Test fun verifyNostrPendingEvent() { - val ots = EventMapper.fromJson(otsPendingEvent) as OtsEvent + val ots = JsonMapper.fromJson(otsPendingEvent) as OtsEvent println(resolver.info(ots.otsByteArray())) assertEquals(null, ots.verify(resolver)) } diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt index 61497dfb17..bc1b7fe22c 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.verify import junit.framework.TestCase @@ -121,7 +121,7 @@ class ThreadDualAxisChartAssemblerTest { fun threadOrderTest() = runBlocking { val eventArray = - EventMapper.mapper.readValue>(db) as List + Event.fromJson(header) + JsonMapper.mapper.readValue>(db) as List + Event.fromJson(header) var counter = 0 eventArray.forEach { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 526d28f9e2..075fd5c45c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent @@ -163,7 +163,7 @@ object LocalPreferences { with(encryptedPreferences()) { val newSystemOfAccounts = getString(PrefKeys.ALL_ACCOUNT_INFO, "[]")?.let { - EventMapper.mapper.readValue>(it) + JsonMapper.mapper.readValue>(it) } if (!newSystemOfAccounts.isNullOrEmpty()) { @@ -183,7 +183,7 @@ object LocalPreferences { savedAccounts.emit(migrated) - edit { putString(PrefKeys.ALL_ACCOUNT_INFO, EventMapper.mapper.writeValueAsString(savedAccounts.value)) } + edit { putString(PrefKeys.ALL_ACCOUNT_INFO, JsonMapper.mapper.writeValueAsString(savedAccounts.value)) } } } } @@ -203,7 +203,7 @@ object LocalPreferences { .edit { putString( PrefKeys.ALL_ACCOUNT_INFO, - EventMapper.mapper.writeValueAsString(accounts.filter { !it.isTransient }), + JsonMapper.mapper.writeValueAsString(accounts.filter { !it.isTransient }), ) } } @@ -310,7 +310,7 @@ object LocalPreferences { putString( PrefKeys.DEFAULT_FILE_SERVER, - EventMapper.mapper.writeValueAsString(settings.defaultFileServer), + JsonMapper.mapper.writeValueAsString(settings.defaultFileServer), ) putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, settings.defaultHomeFollowList.value) putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, settings.defaultStoriesFollowList.value) @@ -324,12 +324,12 @@ object LocalPreferences { ) putString( PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, - EventMapper.mapper.writeValueAsString(settings.zapPaymentRequest?.denormalize()), + JsonMapper.mapper.writeValueAsString(settings.zapPaymentRequest?.denormalize()), ) if (settings.backupContactList != null) { putString( PrefKeys.LATEST_CONTACT_LIST, - EventMapper.mapper.writeValueAsString(settings.backupContactList), + JsonMapper.mapper.writeValueAsString(settings.backupContactList), ) } else { remove(PrefKeys.LATEST_CONTACT_LIST) @@ -338,7 +338,7 @@ object LocalPreferences { if (settings.backupUserMetadata != null) { putString( PrefKeys.LATEST_USER_METADATA, - EventMapper.mapper.writeValueAsString(settings.backupUserMetadata), + JsonMapper.mapper.writeValueAsString(settings.backupUserMetadata), ) } else { remove(PrefKeys.LATEST_USER_METADATA) @@ -347,7 +347,7 @@ object LocalPreferences { if (settings.backupDMRelayList != null) { putString( PrefKeys.LATEST_DM_RELAY_LIST, - EventMapper.mapper.writeValueAsString(settings.backupDMRelayList), + JsonMapper.mapper.writeValueAsString(settings.backupDMRelayList), ) } else { remove(PrefKeys.LATEST_DM_RELAY_LIST) @@ -356,7 +356,7 @@ object LocalPreferences { if (settings.backupNIP65RelayList != null) { putString( PrefKeys.LATEST_NIP65_RELAY_LIST, - EventMapper.mapper.writeValueAsString(settings.backupNIP65RelayList), + JsonMapper.mapper.writeValueAsString(settings.backupNIP65RelayList), ) } else { remove(PrefKeys.LATEST_NIP65_RELAY_LIST) @@ -365,7 +365,7 @@ object LocalPreferences { if (settings.backupSearchRelayList != null) { putString( PrefKeys.LATEST_SEARCH_RELAY_LIST, - EventMapper.mapper.writeValueAsString(settings.backupSearchRelayList), + JsonMapper.mapper.writeValueAsString(settings.backupSearchRelayList), ) } else { remove(PrefKeys.LATEST_SEARCH_RELAY_LIST) @@ -374,7 +374,7 @@ object LocalPreferences { if (settings.backupBlockedRelayList != null) { putString( PrefKeys.LATEST_BLOCKED_RELAY_LIST, - EventMapper.mapper.writeValueAsString(settings.backupBlockedRelayList), + JsonMapper.mapper.writeValueAsString(settings.backupBlockedRelayList), ) } else { remove(PrefKeys.LATEST_BLOCKED_RELAY_LIST) @@ -383,7 +383,7 @@ object LocalPreferences { if (settings.backupTrustedRelayList != null) { putString( PrefKeys.LATEST_TRUSTED_RELAY_LIST, - EventMapper.mapper.writeValueAsString(settings.backupTrustedRelayList), + JsonMapper.mapper.writeValueAsString(settings.backupTrustedRelayList), ) } else { remove(PrefKeys.LATEST_TRUSTED_RELAY_LIST) @@ -398,7 +398,7 @@ object LocalPreferences { if (settings.backupMuteList != null) { putString( PrefKeys.LATEST_MUTE_LIST, - EventMapper.mapper.writeValueAsString(settings.backupMuteList), + JsonMapper.mapper.writeValueAsString(settings.backupMuteList), ) } else { remove(PrefKeys.LATEST_MUTE_LIST) @@ -407,7 +407,7 @@ object LocalPreferences { if (settings.backupPrivateHomeRelayList != null) { putString( PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST, - EventMapper.mapper.writeValueAsString(settings.backupPrivateHomeRelayList), + JsonMapper.mapper.writeValueAsString(settings.backupPrivateHomeRelayList), ) } else { remove(PrefKeys.LATEST_PRIVATE_HOME_RELAY_LIST) @@ -416,7 +416,7 @@ object LocalPreferences { if (settings.backupAppSpecificData != null) { putString( PrefKeys.LATEST_APP_SPECIFIC_DATA, - EventMapper.mapper.writeValueAsString(settings.backupAppSpecificData), + JsonMapper.mapper.writeValueAsString(settings.backupAppSpecificData), ) } else { remove(PrefKeys.LATEST_APP_SPECIFIC_DATA) @@ -425,7 +425,7 @@ object LocalPreferences { if (settings.backupChannelList != null) { putString( PrefKeys.LATEST_CHANNEL_LIST, - EventMapper.mapper.writeValueAsString(settings.backupChannelList), + JsonMapper.mapper.writeValueAsString(settings.backupChannelList), ) } else { remove(PrefKeys.LATEST_CHANNEL_LIST) @@ -434,7 +434,7 @@ object LocalPreferences { if (settings.backupCommunityList != null) { putString( PrefKeys.LATEST_COMMUNITY_LIST, - EventMapper.mapper.writeValueAsString(settings.backupCommunityList), + JsonMapper.mapper.writeValueAsString(settings.backupCommunityList), ) } else { remove(PrefKeys.LATEST_COMMUNITY_LIST) @@ -443,7 +443,7 @@ object LocalPreferences { if (settings.backupHashtagList != null) { putString( PrefKeys.LATEST_HASHTAG_LIST, - EventMapper.mapper.writeValueAsString(settings.backupHashtagList), + JsonMapper.mapper.writeValueAsString(settings.backupHashtagList), ) } else { remove(PrefKeys.LATEST_HASHTAG_LIST) @@ -452,7 +452,7 @@ object LocalPreferences { if (settings.backupGeohashList != null) { putString( PrefKeys.LATEST_HASHTAG_LIST, - EventMapper.mapper.writeValueAsString(settings.backupGeohashList), + JsonMapper.mapper.writeValueAsString(settings.backupGeohashList), ) } else { remove(PrefKeys.LATEST_HASHTAG_LIST) @@ -461,7 +461,7 @@ object LocalPreferences { if (settings.backupEphemeralChatList != null) { putString( PrefKeys.LATEST_EPHEMERAL_LIST, - EventMapper.mapper.writeValueAsString(settings.backupEphemeralChatList), + JsonMapper.mapper.writeValueAsString(settings.backupEphemeralChatList), ) } else { remove(PrefKeys.LATEST_EPHEMERAL_LIST) @@ -475,7 +475,7 @@ object LocalPreferences { remove(PrefKeys.USE_PROXY) remove(PrefKeys.PROXY_PORT) - putString(PrefKeys.TOR_SETTINGS, EventMapper.mapper.writeValueAsString(settings.torSettings.toSettings())) + putString(PrefKeys.TOR_SETTINGS, JsonMapper.mapper.writeValueAsString(settings.torSettings.toSettings())) val regularMap = settings.lastReadPerRoute.value.mapValues { @@ -484,13 +484,13 @@ object LocalPreferences { putString( PrefKeys.LAST_READ_PER_ROUTE, - EventMapper.mapper.writeValueAsString(regularMap), + JsonMapper.mapper.writeValueAsString(regularMap), ) putStringSet(PrefKeys.HAS_DONATED_IN_VERSION, settings.hasDonatedInVersion.value) putString( PrefKeys.PENDING_ATTESTATIONS, - EventMapper.mapper.writeValueAsString(settings.pendingAttestations.value), + JsonMapper.mapper.writeValueAsString(settings.pendingAttestations.value), ) } } @@ -506,7 +506,7 @@ object LocalPreferences { ) { Log.d("LocalPreferences", "Saving to shared settings") prefs.edit { - putString(PrefKeys.SHARED_SETTINGS, EventMapper.mapper.writeValueAsString(sharedSettings)) + putString(PrefKeys.SHARED_SETTINGS, JsonMapper.mapper.writeValueAsString(sharedSettings)) } } @@ -514,7 +514,7 @@ object LocalPreferences { Log.d("LocalPreferences", "Load shared settings") with(prefs) { return try { - getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { EventMapper.mapper.readValue(it) } + getString(PrefKeys.SHARED_SETTINGS, "{}")?.let { JsonMapper.mapper.readValue(it) } } catch (e: Throwable) { if (e is CancellationException) throw e Log.w( @@ -658,7 +658,7 @@ object LocalPreferences { if (T::class.java.isInstance(Event::class.java)) { Event.fromJson(value) as T? } else { - EventMapper.mapper.readValue(value) + JsonMapper.mapper.readValue(value) } } catch (e: Throwable) { if (e is CancellationException) throw e diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt index da6285e74f..e76ee05226 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AccountSyncedSettingsInternal import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import kotlinx.coroutines.CoroutineScope @@ -55,7 +55,7 @@ class AppSpecificState( fun saveNewAppSpecificData(onDone: (AppSpecificDataEvent) -> Unit) { val toInternal = settings.syncedSettings.toInternal() - signer.nip44Encrypt(EventMapper.mapper.writeValueAsString(toInternal), signer.pubKey) { encrypted -> + signer.nip44Encrypt(JsonMapper.mapper.writeValueAsString(toInternal), signer.pubKey) { encrypted -> AppSpecificDataEvent.create( dTag = APP_SPECIFIC_DATA_D_TAG, description = encrypted, @@ -74,7 +74,7 @@ class AppSpecificState( LocalCache.justConsumeMyOwnEvent(event) signer.decrypt(event.content, event.pubKey) { decrypted -> try { - val syncedSettings = EventMapper.mapper.readValue(decrypted) + val syncedSettings = JsonMapper.mapper.readValue(decrypted) settings.syncedSettings.updateFrom(syncedSettings) } catch (e: Throwable) { if (e is CancellationException) throw e @@ -94,7 +94,7 @@ class AppSpecificState( signer.decrypt(it.content, it.pubKey) { decrypted -> val syncedSettings = try { - EventMapper.mapper.readValue(decrypted) + JsonMapper.mapper.readValue(decrypted) } catch (e: Throwable) { if (e is CancellationException) throw e Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt index 2c171ecee9..2e0bf231a4 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/CacheBenchmark.kt @@ -27,7 +27,7 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.utils.LargeCache import org.junit.Assert.assertTrue import org.junit.Rule @@ -44,7 +44,7 @@ open class BaseCacheBenchmark { // This file includes duplicates val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") - return EventMapper.mapper.readValue>( + return JsonMapper.mapper.readValue>( GZIPInputStream(fullDBInputStream), ) } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt index e15b499861..0820f45607 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt @@ -24,7 +24,7 @@ import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.EventFactory -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.nip01Core.verifyId import com.vitorpamplona.quartz.nip01Core.verifySignature @@ -47,28 +47,28 @@ class EventBenchmark { @Test fun parseComplete() { benchmarkRule.measureRepeated { - val tree = EventMapper.mapper.readTree(reqResponseEvent) - val event = EventMapper.fromJson(tree[2]) + val tree = JsonMapper.mapper.readTree(reqResponseEvent) + val event = JsonMapper.fromJson(tree[2]) assertTrue(event.verify()) } } @Test fun parseREQString() { - benchmarkRule.measureRepeated { EventMapper.mapper.readTree(reqResponseEvent) } + benchmarkRule.measureRepeated { JsonMapper.mapper.readTree(reqResponseEvent) } } @Test fun parseEvent() { - val msg = EventMapper.mapper.readTree(reqResponseEvent) + val msg = JsonMapper.mapper.readTree(reqResponseEvent) - benchmarkRule.measureRepeated { EventMapper.fromJson(msg[2]) } + benchmarkRule.measureRepeated { JsonMapper.fromJson(msg[2]) } } @Test fun checkId() { - val msg = EventMapper.mapper.readTree(reqResponseEvent) - val event = EventMapper.fromJson(msg[2]) + val msg = JsonMapper.mapper.readTree(reqResponseEvent) + val event = JsonMapper.fromJson(msg[2]) benchmarkRule.measureRepeated { // Should pass assertTrue(event.verifyId()) @@ -77,8 +77,8 @@ class EventBenchmark { @Test fun checkSignature() { - val msg = EventMapper.mapper.readTree(reqResponseEvent) - val event = EventMapper.fromJson(msg[2]) + val msg = JsonMapper.mapper.readTree(reqResponseEvent) + val event = JsonMapper.fromJson(msg[2]) benchmarkRule.measureRepeated { // Should pass assertTrue(event.verifySignature()) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdHasherBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdHasherBenchmark.kt index ec0e198362..b343ff0c1d 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdHasherBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventCmdHasherBenchmark.kt @@ -25,7 +25,7 @@ import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.generateId -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.verifyId import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue @@ -45,7 +45,7 @@ class EventCmdHasherBenchmark { @Test fun checkIDHashKind1WihtoutTags() { - val event = EventMapper.fromJson(EventMapper.mapper.readTree(reqResponseEvent)) + val event = JsonMapper.fromJson(JsonMapper.mapper.readTree(reqResponseEvent)) benchmarkRule.measureRepeated { // Should pass diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt index be316563cf..a64b13d845 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt @@ -27,7 +27,7 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.utils.LargeCache import org.junit.Assert.assertTrue import org.junit.Rule @@ -42,7 +42,7 @@ open class BaseLargeCacheBenchmark { // This file includes duplicates val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") - return EventMapper.mapper.readValue>( + return JsonMapper.mapper.readValue>( GZIPInputStream(fullDBInputStream), ) } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt index 8882836bbd..b40314fd7d 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/LargeDBSignatureCheck.kt @@ -24,7 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.verify import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue @@ -42,7 +42,7 @@ class LargeDBSignatureCheck { val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_short.json") val eventArray = - EventMapper.mapper.readValue>( + JsonMapper.mapper.readValue>( InputStreamReader(fullDBInputStream), ) as List @@ -62,7 +62,7 @@ class LargeDBSignatureCheck { val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") val eventArray = - EventMapper.mapper.readValue>( + JsonMapper.mapper.readValue>( GZIPInputStream(fullDBInputStream), ) as List diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt index b16fc94a28..23a80683bf 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import org.junit.Test import org.junit.runner.RunWith @@ -52,8 +52,8 @@ class EventSigCheck { @Test fun testUnicode2028and2029ShouldNotBeEscaped() { - val msg = EventMapper.mapper.readTree(payload1) - val event = EventMapper.fromJson(msg[2]) + val msg = JsonMapper.mapper.readTree(payload1) + val event = JsonMapper.fromJson(msg[2]) // Should pass event.checkSignature() diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt index e7da75dd8d..ca39a13545 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt @@ -24,7 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEncryption.Companion.createEncryptionPrivateKey import com.vitorpamplona.quartz.nip59GiftWraps.wait1SecondForResult @@ -39,7 +39,7 @@ class PrivateZapTests { @Test fun testPollZap() { val poll = - EventMapper.fromJson( + JsonMapper.fromJson( """{ "content": "New poll \n\n #zappoll", "created_at": 1682440713, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/WaveformTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/WaveformTag.kt index 3c1c0e8fa1..7813f78b92 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/WaveformTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/audio/header/tags/WaveformTag.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.experimental.audio.header.tags import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.has -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.utils.ensure class WaveformTag( @@ -38,12 +38,12 @@ class WaveformTag( ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - val wave = runCatching { EventMapper.mapper.readValue>(tag[1]) }.getOrNull() + val wave = runCatching { JsonMapper.mapper.readValue>(tag[1]) }.getOrNull() if (wave.isNullOrEmpty()) return null return WaveformTag(wave) } @JvmStatic - fun assemble(wave: List) = arrayOf(TAG_NAME, EventMapper.mapper.writeValueAsString(wave)) + fun assemble(wave: List) = arrayOf(TAG_NAME, JsonMapper.mapper.writeValueAsString(wave)) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt index fe6ce61b9d..50f10e1949 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.experimental.edits.tags.RelayTag import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag @@ -81,7 +81,7 @@ class PrivateOutboxRelayListEvent( try { signer.nip44Decrypt(content, pubKey) { try { - privateTagsCache = EventMapper.mapper.readValue(it) + privateTagsCache = JsonMapper.mapper.readValue(it) privateTagsCache?.let { onReady(it) } } catch (e: Throwable) { Log.w("PrivateOutboxRelayListEvent", "Error parsing the JSON: ${e.message}. Json `$it` from event `${toNostrUri()}`") @@ -107,7 +107,7 @@ class PrivateOutboxRelayListEvent( signer: NostrSigner, onReady: (String) -> Unit, ) { - val msg = EventMapper.mapper.writeValueAsString(privateTags) + val msg = JsonMapper.mapper.writeValueAsString(privateTags) signer.nip44Encrypt( msg, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt index 8403328d32..f744f3ff96 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt @@ -21,9 +21,8 @@ package com.vitorpamplona.quartz.nip01Core.core import androidx.compose.runtime.Immutable -import com.fasterxml.jackson.annotation.JsonProperty import com.vitorpamplona.quartz.nip01Core.jackson.EventManualSerializer -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory @@ -32,8 +31,8 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable open class Event( val id: HexKey, - @JsonProperty("pubkey") val pubKey: HexKey, - @JsonProperty("created_at") val createdAt: Long, + val pubKey: HexKey, + val createdAt: Long, val kind: Int, val tags: TagArray, val content: String, @@ -57,7 +56,7 @@ open class Event( fun toJson(): String = EventManualSerializer.toJson(id, pubKey, createdAt, kind, tags, content, sig) companion object { - fun fromJson(json: String): Event = EventMapper.fromJson(json) + fun fromJson(json: String): Event = JsonMapper.fromJson(json) fun build( kind: Int, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt index 1d113d1b8b..5fcfd9e8cd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt @@ -24,7 +24,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.utils.sha256.sha256 class EventHasher { @@ -61,7 +61,7 @@ class EventHasher { kind: Int, tags: Array>, content: String, - ): String = EventMapper.toJson(makeJsonObjectForId(pubKey, createdAt, kind, tags, content)) + ): String = JsonMapper.toJson(makeJsonObjectForId(pubKey, createdAt, kind, tags, content)) fun hashIdBytes( pubKey: HexKey, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt index e1b49ed4ae..5351028e0a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt @@ -67,7 +67,7 @@ class EventManualSerializer { sig: String, ): String { val obj = assemble(id, pubKey, createdAt, kind, tags, content, sig) - return EventMapper.mapper.writeValueAsString(obj) + return JsonMapper.mapper.writeValueAsString(obj) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt similarity index 98% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt index 9794d9bb94..004812514c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt @@ -39,8 +39,8 @@ import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorSerializer -class EventMapper { - companion object { +class JsonMapper { + companion object Companion { val defaultPrettyPrinter = InliningTagArrayPrettyPrinter() val mapper = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt index 80363907ab..67e8266a40 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.builder -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.metadata.tags.AboutTag import com.vitorpamplona.quartz.nip01Core.metadata.tags.BannerTag import com.vitorpamplona.quartz.nip01Core.metadata.tags.DisplayNameTag @@ -63,7 +63,7 @@ class MetadataEvent( fun contactMetaData() = try { - EventMapper.mapper.readValue(content, UserMetadata::class.java) + JsonMapper.mapper.readValue(content, UserMetadata::class.java) } catch (e: Exception) { Log.w("MetadataEvent", "Content Parse Error: ${toNostrUri()} ${e.localizedMessage}") null diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/EventMessage.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/EventMessage.kt index c161f87d9f..ee71c2381d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/EventMessage.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/EventMessage.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient import com.fasterxml.jackson.databind.JsonNode import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper class EventMessage( val subId: String, @@ -35,7 +35,7 @@ class EventMessage( fun parse(msgArray: JsonNode): EventMessage = EventMessage( msgArray.get(1).asText(), - EventMapper.fromJson(msgArray.get(2)), + JsonMapper.fromJson(msgArray.get(2)), ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/ToClientParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/ToClientParser.kt index 7258602fe9..f324c42c0c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/ToClientParser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/ToClientParser.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.commands.toClient -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper class ToClientParser { fun parse(newMessage: String): Message? { - val msgArray = EventMapper.mapper.readTree(newMessage) + val msgArray = JsonMapper.mapper.readTree(newMessage) val type = msgArray.get(0).asText() return when (type) { EventMessage.LABEL -> EventMessage.parse(msgArray) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt index 612a9c5ca0..95c11e8889 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/AuthCmd.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay import com.fasterxml.jackson.databind.JsonNode -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent class AuthCmd( @@ -36,7 +36,7 @@ class AuthCmd( @JvmStatic fun parse(msgArray: JsonNode): AuthCmd = AuthCmd( - EventMapper.fromJson(msgArray.get(1)) as RelayAuthEvent, + JsonMapper.fromJson(msgArray.get(1)) as RelayAuthEvent, ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CountCmd.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CountCmd.kt index 4e631893c2..7c9c7cc4e6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CountCmd.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/CountCmd.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ObjectNode -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterDeserializer import com.vitorpamplona.quartz.utils.joinToStringLimited @@ -54,7 +54,7 @@ class CountCmd( val filters = mutableListOf() for (i in 2 until msgArray.size()) { - val json = EventMapper.mapper.readTree(msgArray.get(i).asText()) + val json = JsonMapper.mapper.readTree(msgArray.get(i).asText()) if (json is ObjectNode) { filters.add(FilterDeserializer.fromJson(json)) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/EventCmd.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/EventCmd.kt index cc7dae3b1d..ea8349a246 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/EventCmd.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/EventCmd.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay import com.fasterxml.jackson.databind.JsonNode import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper class EventCmd( val event: Event, @@ -36,7 +36,7 @@ class EventCmd( @JvmStatic fun parse(msgArray: JsonNode): EventCmd = EventCmd( - EventMapper.fromJson(msgArray.get(1)), + JsonMapper.fromJson(msgArray.get(1)), ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt index 08390c0a86..01439d24b6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ReqCmd.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.ObjectNode -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterDeserializer import com.vitorpamplona.quartz.utils.joinToStringLimited @@ -54,7 +54,7 @@ class ReqCmd( val filters = mutableListOf() for (i in 2 until msgArray.size()) { - val json = EventMapper.mapper.readTree(msgArray.get(i).asText()) + val json = JsonMapper.mapper.readTree(msgArray.get(i).asText()) if (json is ObjectNode) { filters.add(FilterDeserializer.fromJson(json)) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ToRelayParser.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ToRelayParser.kt index 97ebf536d6..1f0e558793 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ToRelayParser.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/commands/toRelay/ToRelayParser.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper class ToRelayParser { fun parse(newMessage: String): Command? { - val msgArray = EventMapper.mapper.readTree(newMessage) + val msgArray = JsonMapper.mapper.readTree(newMessage) val type = msgArray.get(0).asText() return when (type) { AuthCmd.LABEL -> AuthCmd.parse(msgArray) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt index dc1a90ffc6..8756646402 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/FilterSerializer.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.filters import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper object FilterSerializer { fun toJsonObject( @@ -80,7 +80,7 @@ object FilterSerializer { limit: Int? = null, search: String? = null, ): String = - EventMapper.mapper.writeValueAsString( + JsonMapper.mapper.writeValueAsString( toJsonObject( ids, authors, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/EventTemplate.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/EventTemplate.kt index 6b7a0b9781..25a2196d67 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/EventTemplate.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/EventTemplate.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.builder import com.vitorpamplona.quartz.nip01Core.core.tagArray -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.utils.TimeUtils class EventTemplate( @@ -36,10 +36,10 @@ class EventTemplate( val tags: TagArray, val content: String, ) { - fun toJson(): String = EventMapper.mapper.writeValueAsString(this) + fun toJson(): String = JsonMapper.mapper.writeValueAsString(this) companion object { - fun fromJson(json: String): EventTemplate = EventTemplateManualDeserializer.fromJson(EventMapper.mapper.readTree(json)) + fun fromJson(json: String): EventTemplate = EventTemplateManualDeserializer.fromJson(JsonMapper.mapper.readTree(json)) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/RelaySet.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/RelaySet.kt index 1ed15824a5..5910c392c9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/RelaySet.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/RelaySet.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip02FollowList import android.util.Log import com.fasterxml.jackson.module.kotlin.readValue -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper data class ReadWrite( val read: Boolean, @@ -31,12 +31,12 @@ data class ReadWrite( class RelaySet { companion object { - fun assemble(relayUse: Map): String = EventMapper.mapper.writeValueAsString(relayUse) + fun assemble(relayUse: Map): String = JsonMapper.mapper.writeValueAsString(relayUse) fun parse(content: String): Map? = try { if (content.isNotEmpty()) { - EventMapper.mapper.readValue>(content) + JsonMapper.mapper.readValue>(content) } else { null } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05.kt index b96439acd4..eb9404e142 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip05DnsIdentifiers/Nip05.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.quartz.nip05DnsIdentifiers -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import kotlinx.coroutines.CancellationException class Nip05 { @@ -46,7 +46,7 @@ class Nip05 { // lowercase version of the username. val nip05url = try { - EventMapper.mapper.readTree(returnBody.lowercase()) + JsonMapper.mapper.readTree(returnBody.lowercase()) } catch (e: Throwable) { if (e is CancellationException) throw e return Result.failure(e) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt index bdd0c2a0b6..b93684d5cc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/base/ChannelData.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip28PublicChat.base import androidx.compose.runtime.Immutable import com.fasterxml.jackson.module.kotlin.readValue -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer @@ -38,9 +38,9 @@ data class ChannelData( fun normalize() = ChannelDataNorm(name, about, picture, relays?.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }) companion object { - fun parse(content: String): ChannelData? = EventMapper.mapper.readValue(content) + fun parse(content: String): ChannelData? = JsonMapper.mapper.readValue(content) - fun assemble(data: ChannelData) = EventMapper.mapper.writeValueAsString(data) + fun assemble(data: ChannelData) = JsonMapper.mapper.writeValueAsString(data) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt index 69c301612f..7627fe0e24 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip44Encryption import android.util.Log -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04 import java.util.Base64 @@ -66,7 +66,7 @@ object Nip44 { // Ignores if it is not a valid json val info = try { - EventMapper.mapper.readValue(json, EncryptedInfoString::class.java) + JsonMapper.mapper.readValue(json, EncryptedInfoString::class.java) } catch (e: Exception) { Log.e("NIP44", "Unable to parse json $json") return null diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseGetRelays.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseGetRelays.kt index 3b497a45b2..6ed8c94e84 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseGetRelays.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseGetRelays.kt @@ -21,19 +21,19 @@ package com.vitorpamplona.quartz.nip46RemoteSigner import com.fasterxml.jackson.core.type.TypeReference -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip02FollowList.ReadWrite import java.util.UUID class BunkerResponseGetRelays( id: String = UUID.randomUUID().toString(), val relays: Map, -) : BunkerResponse(id, EventMapper.mapper.writeValueAsString(relays), null) { +) : BunkerResponse(id, JsonMapper.mapper.writeValueAsString(relays), null) { companion object { fun parse( id: String, result: String, error: String? = null, - ) = BunkerResponseGetRelays(id, EventMapper.mapper.readValue(result, object : TypeReference>() {})) + ) = BunkerResponseGetRelays(id, JsonMapper.mapper.readValue(result, object : TypeReference>() {})) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt index d092ea8fae..231c88a10a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip46RemoteSigner import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.Hex @@ -73,7 +73,7 @@ class NostrConnectEvent( // decrypts using NIP-04 or NIP-44 signer.decrypt(content, talkingWith(signer.pubKey)) { retVal -> - val content = EventMapper.mapper.readValue(retVal, BunkerMessage::class.java) + val content = JsonMapper.mapper.readValue(retVal, BunkerMessage::class.java) decryptedContent = decryptedContent + Pair(signer.pubKey, content) @@ -98,7 +98,7 @@ class NostrConnectEvent( arrayOf("p", remoteKey), ) - val encrypted = EventMapper.mapper.writeValueAsString(message) + val encrypted = JsonMapper.mapper.writeValueAsString(message) signer.nip44Encrypt(encrypted, remoteKey) { content -> signer.sign(createdAt, KIND, tags, content, onReady) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt index 064a934de6..884f6aa90d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt @@ -24,7 +24,7 @@ import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -62,7 +62,7 @@ class LnZapPaymentRequestEvent( try { signer.decrypt(content, talkingWith(signer.pubKey)) { jsonText -> - val payInvoiceMethod = EventMapper.mapper.readValue(jsonText, Request::class.java) + val payInvoiceMethod = JsonMapper.mapper.readValue(jsonText, Request::class.java) lnInvoice = (payInvoiceMethod as? PayInvoiceMethod)?.params?.invoice @@ -84,7 +84,7 @@ class LnZapPaymentRequestEvent( createdAt: Long = TimeUtils.now(), onReady: (LnZapPaymentRequestEvent) -> Unit, ) { - val serializedRequest = EventMapper.mapper.writeValueAsString(PayInvoiceMethod.create(lnInvoice)) + val serializedRequest = JsonMapper.mapper.writeValueAsString(PayInvoiceMethod.create(lnInvoice)) val tags = arrayOf(arrayOf("p", walletServicePubkey), AltTag.assemble(ALT)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt index e2a2272367..c065a2a27c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt @@ -24,7 +24,7 @@ import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -71,7 +71,7 @@ class LnZapPaymentResponseEvent( try { if (content.isNotEmpty()) { plainContent(signer) { - EventMapper.mapper.readValue(it, Response::class.java)?.let { + JsonMapper.mapper.readValue(it, Response::class.java)?.let { response = it onReady(it) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt index 20e7738402..95dac426cf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.isTagged import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address @@ -176,7 +176,7 @@ abstract class GeneralListEvent( signer: NostrSigner, onReady: (String) -> Unit, ) { - val msg = EventMapper.mapper.writeValueAsString(privateTags) + val msg = JsonMapper.mapper.writeValueAsString(privateTags) signer.nip04Encrypt( msg, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.kt index 28215447ab..123dbb7f7f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.quartz.nip51Lists.encryption import com.fasterxml.jackson.module.kotlin.readValue -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner class PrivateTagsInContent { companion object { - fun decode(content: String) = EventMapper.mapper.readValue>>(content) + fun decode(content: String) = JsonMapper.mapper.readValue>>(content) - fun encode(privateTags: Array>) = EventMapper.mapper.writeValueAsString(privateTags) + fun encode(privateTags: Array>) = JsonMapper.mapper.writeValueAsString(privateTags) fun decrypt( content: String, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt index 93d2cf6b01..1a133acb3b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/Rumor.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.EventFactory import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent class Rumor( @@ -49,9 +49,9 @@ class Rumor( } companion object { - fun fromJson(json: String): Rumor = EventMapper.mapper.readValue(json, Rumor::class.java) + fun fromJson(json: String): Rumor = JsonMapper.mapper.readValue(json, Rumor::class.java) - fun toJson(event: Rumor): String = EventMapper.mapper.writeValueAsString(event) + fun toJson(event: Rumor): String = JsonMapper.mapper.writeValueAsString(event) fun create(event: Event): Rumor = Rumor(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt index 654dad7be4..2857593eb6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip89AppHandlers.definition import androidx.compose.runtime.Stable import com.fasterxml.jackson.annotation.JsonProperty -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -113,8 +113,8 @@ class AppMetadata { fun toJson() = assemble(this) companion object { - fun assemble(data: AppMetadata) = EventMapper.mapper.writeValueAsString(data) + fun assemble(data: AppMetadata) = JsonMapper.mapper.writeValueAsString(data) - fun parse(content: String) = EventMapper.mapper.readValue(content, AppMetadata::class.java) + fun parse(content: String) = JsonMapper.mapper.readValue(content, AppMetadata::class.java) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt index c781ebcada..da692ad583 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt @@ -25,7 +25,7 @@ import androidx.compose.runtime.Immutable import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent @@ -59,7 +59,7 @@ class NIP90ContentDiscoveryResponseEvent( try { events = - EventMapper.mapper.readValue>>(content).mapNotNull { + JsonMapper.mapper.readValue>>(content).mapNotNull { if (it.size > 1 && it[0] == "e") { it[1] } else { diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt index 3d1fc91191..674d72bb02 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.metadata import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.jackson.EventManualSerializer -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.utils.nsecToSigner import org.junit.Assert.assertEquals import org.junit.Test @@ -35,7 +35,7 @@ class UpdateMetadataTest { */ fun Event.toPrettyJson(): String { val obj = EventManualSerializer.assemble(id, pubKey, createdAt, kind, tags, content, sig) - return EventMapper.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj) + return JsonMapper.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj) } @Test diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestTest.kt index 3d94e0c4b4..9202392cbd 100644 --- a/quartz/src/test/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestTest.kt +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequestTest.kt @@ -20,14 +20,14 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import org.junit.Test class BunkerRequestTest { @Test fun testBunkerRequestDeSerialization() { val requestJson = """{"id":"123","method":"sign_event","params":["{\"created_at\":1234,\"kind\":1,\"tags\":[],\"content\":\"This is an unsigned event.\"}"]}""" - val bunkerRequest = EventMapper.mapper.readValue(requestJson, BunkerRequest::class.java) + val bunkerRequest = JsonMapper.mapper.readValue(requestJson, BunkerRequest::class.java) assert(bunkerRequest is BunkerRequestSign) assert((bunkerRequest as BunkerRequestSign).event.kind == 1) From 510583e72e9805ba144db204a87ddf8fb290176e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Jul 2025 11:17:59 -0400 Subject: [PATCH 092/185] Rewrites NIP-55 to start offering better error handling. --- .../amethyst/ImageUploadTesting.kt | 6 +- .../ThreadDualAxisChartAssemblerTest.kt | 9 +- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/AccountSettings.kt | 50 +- .../amethyst/service/PackageUtils.kt | 12 - .../EventNotificationConsumer.kt | 23 +- .../service/notifications/RegisterAccounts.kt | 12 +- .../ui/screen/loggedIn/AccountViewModel.kt | 10 +- .../ui/screen/loggedIn/LoggedInPage.kt | 64 +- .../screen/loggedOff/LoginOrSignupScreen.kt | 2 + .../ui/screen/loggedOff/LoginScreen.kt | 814 ------------------ .../ui/screen/loggedOff/TorSettingsSetup.kt | 4 - .../loggedOff/login/ExternalSignerButton.kt | 84 ++ .../ui/screen/loggedOff/login/KeyTextField.kt | 161 ++++ .../ui/screen/loggedOff/login/LoginButton.kt | 60 ++ .../loggedOff/login/LoginErrorManager.kt | 55 ++ .../ui/screen/loggedOff/login/LoginScreen.kt | 362 ++++++++ .../screen/loggedOff/login/LoginViewModel.kt | 157 ++++ .../loggedOff/login/LoginWithAmberButton.kt | 52 ++ .../loggedOff/login/OpenURIIfNotLoggedIn.kt | 116 +++ .../ui/screen/loggedOff/login/SignUpButton.kt | 48 ++ .../ui/screen/loggedOff/signup/LoginButton.kt | 48 ++ .../screen/loggedOff/signup/SignUpButton.kt | 52 ++ .../loggedOff/{ => signup}/SignUpScreen.kt | 132 +-- .../loggedOff/signup/SignUpViewModel.kt | 86 ++ amethyst/src/main/res/values/strings.xml | 1 + .../quartz/nip01Core/core/Event.kt | 9 + .../quartz/nip01Core/jackson/JsonMapper.kt | 8 +- .../quartz/nip01Core/signers/NostrSigner.kt | 8 +- .../nip01Core/signers/NostrSignerInternal.kt | 16 +- .../ExternalSignerLauncher.kt | 438 ---------- .../nip55AndroidSigner/api/CommandType.kt | 50 ++ .../nip55AndroidSigner/api/SignerResult.kt | 86 ++ .../api/background/queries/DecryptZapQuery.kt | 66 ++ .../api/background/queries/DeriveKeyQuery.kt | 51 ++ .../api/background/queries/LoginQuery.kt | 53 ++ .../background/queries/Nip04DecryptQuery.kt | 54 ++ .../background/queries/Nip04EncryptQuery.kt | 54 ++ .../background/queries/Nip44DecryptQuery.kt | 54 ++ .../background/queries/Nip44EncryptQuery.kt | 54 ++ .../api/background/queries/SignQuery.kt | 86 ++ .../background/utils/ContentResolverExt.kt | 56 ++ .../api/background/utils/CursorExt.kt | 33 + .../api/foreground/IntentRequestDatabase.kt | 65 ++ .../api/foreground/NewResultProcessor.kt | 47 + .../intents/requests/DecryptZapRequest.kt | 43 + .../intents/requests/DeriveKeyRequest.kt | 42 + .../intents/requests/LoginRequest.kt | 49 ++ .../intents/requests/Nip04DecryptRequest.kt | 44 + .../intents/requests/Nip04EncryptRequest.kt | 44 + .../intents/requests/Nip44DecryptRequest.kt | 44 + .../intents/requests/Nip44EncryptRequest.kt | 44 + .../intents/requests/SignRequest.kt | 43 + .../intents/responses/DecryptZapResponse.kt | 55 ++ .../intents/responses/DeriveKeyResponse.kt | 38 + .../intents/responses/LoginResponse.kt | 45 + .../intents/responses/Nip04DecryptResponse.kt | 38 + .../intents/responses/Nip04EncryptResponse.kt | 38 + .../intents/responses/Nip44DecryptResponse.kt | 38 + .../intents/responses/Nip44EncryptResponse.kt | 38 + .../intents/responses/SignResponse.kt | 76 ++ .../processors/DecryptZapResultProcessor.kt | 40 + .../processors/DeriveKeyResultProcessor.kt | 39 + .../processors/LoginResultProcessor.kt | 40 + .../processors/Nip04DecryptResultProcessor.kt | 39 + .../processors/Nip04EncryptResultProcessor.kt | 39 + .../processors/Nip44DecryptResultProcessor.kt | 39 + .../processors/Nip44EncryptResultProcessor.kt | 38 + .../processors/SignResultProcessor.kt | 41 + .../api/permission/Permission.kt | 37 + .../api/permission/PermissionDeserializer.kt | 40 + .../api/permission/PermissionSerializer.kt | 42 + .../client/ExternalSignerLogin.kt | 38 + .../client/IActivityLauncher.kt | 31 + .../client/IsExternalSignerInstalled.kt | 37 + .../{ => client}/NostrSignerExternal.kt | 124 ++- .../handlers/BackgroundRequestHandler.kt | 163 ++++ .../handlers/ForegroundRequestHandler.kt | 108 +++ 78 files changed, 3809 insertions(+), 1555 deletions(-) delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextField.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginWithAmberButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedIn.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/SignUpButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/LoginButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpButton.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/{ => signup}/SignUpScreen.kt (64%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/CommandType.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DeriveKeyQuery.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/LoginQuery.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04DecryptQuery.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04EncryptQuery.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44DecryptQuery.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44EncryptQuery.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/SignQuery.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/ContentResolverExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/CursorExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DecryptZapRequest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DeriveKeyRequest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/LoginRequest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip04DecryptRequest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip04EncryptRequest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip44DecryptRequest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip44EncryptRequest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/SignRequest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/Permission.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/PermissionDeserializer.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/PermissionSerializer.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IActivityLauncher.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IsExternalSignerInstalled.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/{ => client}/NostrSignerExternal.kt (53%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index d891ebecae..acd0939e90 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.utils.sha256.sha256 import junit.framework.TestCase.assertEquals import junit.framework.TestCase.fail @@ -55,9 +56,12 @@ import kotlin.random.Random @RunWith(AndroidJUnit4::class) class ImageUploadTesting { companion object { + val accountSettings = AccountSettings(KeyPair()) + val account = Account( - AccountSettings(KeyPair()), + settings = accountSettings, + signer = NostrSignerInternal(accountSettings.keyPair), scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), ) } diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt index bc1b7fe22c..8a028d15e0 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFi import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.verify import junit.framework.TestCase @@ -138,7 +139,13 @@ class ThreadDualAxisChartAssemblerTest { null, ) - val account = Account(AccountSettings(KeyPair()), scope = CoroutineScope(Dispatchers.IO + SupervisorJob())) + val keyPair = KeyPair() + val account = + Account( + settings = AccountSettings(keyPair = keyPair), + signer = NostrSignerInternal(keyPair), + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + ) withContext(Dispatchers.Main) { val user = account.userProfile().flow() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index d51d045b14..92e400490b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -222,7 +222,7 @@ import kotlin.coroutines.resume @Stable class Account( val settings: AccountSettings = AccountSettings(KeyPair()), - val signer: NostrSigner = settings.createSigner(), + val signer: NostrSigner, geolocationFlow: StateFlow, val cache: LocalCache, val client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index be838df741..58dc27e8b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -20,9 +20,8 @@ */ package com.vitorpamplona.amethyst.model -import android.util.Log +import android.content.ContentResolver import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.screen.FeedDefinition @@ -39,6 +38,8 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent @@ -46,8 +47,9 @@ import com.vitorpamplona.quartz.nip51Lists.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent -import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncher -import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission +import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo @@ -61,14 +63,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import java.util.Locale -val DefaultChannelSet = - setOf( - // Anigma's Nostr - "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", - // Amethyst's Group - "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", - ) - val DefaultChannels = listOf( // Anigma's Nostr @@ -90,6 +84,17 @@ val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos) val DefaultSearchRelayList = setOf(Constants.band, Constants.wine, Constants.where, Constants.nostoday) +val DefaultSignerPermissions = + listOf( + Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND), + Permission(CommandType.SIGN_EVENT, DraftEvent.KIND), + Permission(CommandType.NIP04_ENCRYPT), + Permission(CommandType.NIP04_DECRYPT), + Permission(CommandType.NIP44_DECRYPT), + Permission(CommandType.NIP44_DECRYPT), + Permission(CommandType.DECRYPT_ZAP_EVENT), + ) + // This has spaces to avoid mixing with a potential NIP-51 list with the same name. val GLOBAL_FOLLOWS = " Global " @@ -147,25 +152,18 @@ class AccountSettings( fun isWriteable(): Boolean = keyPair.privKey != null || externalSignerPackageName != null - fun createSigner() = + fun createSigner(contentResolver: ContentResolver) = if (keyPair.privKey != null) { NostrSignerInternal(keyPair) } else { when (val packageName = externalSignerPackageName) { null -> NostrSignerInternal(keyPair) - else -> { - val externalSignerLauncher = ExternalSignerLauncher(keyPair.pubKey.toHexKey(), packageName) - // TODO: How to handle the launcher here? - try { - externalSignerLauncher.registerLauncher( - launcher = { }, - contentResolver = Amethyst.instance::contentResolverFn, - ) - } catch (e: Exception) { - Log.d("AccountSettings", "Failed to initialize external signer", e) - } - NostrSignerExternal(keyPair.pubKey.toHexKey(), externalSignerLauncher) - } + else -> + NostrSignerExternal( + pubKey = keyPair.pubKey.toHexKey(), + packageName = packageName, + contentResolver = contentResolver, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt index 27ec2f6d3d..e21d1759b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/PackageUtils.kt @@ -22,8 +22,6 @@ package com.vitorpamplona.amethyst.service import android.annotation.SuppressLint import android.content.Context -import android.content.Intent -import androidx.core.net.toUri object PackageUtils { @SuppressLint("QueryPermissionsNeeded") @@ -36,14 +34,4 @@ object PackageUtils { } != null fun isOrbotInstalled(context: Context): Boolean = isPackageInstalled(context, "org.torproject.android") - - fun isExternalSignerInstalled(context: Context): Boolean { - val intent = - Intent().apply { - action = Intent.ACTION_VIEW - data = "nostrsigner:".toUri() - } - val infos = context.packageManager.queryIntentActivities(intent, 0) - return infos.size > 0 - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index d5572ffe4c..cfd928f67c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -24,7 +24,6 @@ import android.app.NotificationManager import android.content.Context import android.util.Log import androidx.core.content.ContextCompat -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AccountSettings @@ -44,7 +43,6 @@ import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri import com.vitorpamplona.quartz.nip37Drafts.DraftEvent -import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent @@ -86,15 +84,7 @@ class EventNotificationConsumer( pushWrappedEvent: GiftWrapEvent, account: AccountSettings, ) { - // TODO: Modify the external launcher to launch as different users. - // Right now it only registers if Amber has already approved this signature - val signer = account.createSigner() - if (signer is NostrSignerExternal) { - signer.launcher.registerLauncher( - launcher = { }, - contentResolver = Amethyst.instance::contentResolverFn, - ) - } + val signer = account.createSigner(applicationContext.contentResolver) pushWrappedEvent.unwrapThrowing(signer) { notificationEvent -> consumeNotificationEvent(notificationEvent, signer, account) @@ -144,16 +134,7 @@ class EventNotificationConsumer( LocalPreferences.loadCurrentAccountFromEncryptedStorage(it.npub)?.let { acc -> Log.d(TAG, "New Notification Testing if for ${it.npub}") try { - // TODO: Modify the external launcher to launch as different users. - // Right now it only registers if Amber has already approved this signature - val signer = acc.createSigner() - if (signer is NostrSignerExternal) { - signer.launcher.registerLauncher( - launcher = { }, - contentResolver = Amethyst.instance::contentResolverFn, - ) - } - + val signer = acc.createSigner(applicationContext.contentResolver) consumeNotificationEvent(event, signer, acc) matchAccount = true } catch (e: Exception) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index 14001c6a2a..b22599810a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent -import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal import com.vitorpamplona.quartz.utils.mapNotNullAsync import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.Dispatchers @@ -63,16 +62,7 @@ class RegisterAccounts( return mapNotNullAsync(remainingTos) { info -> tryAndWait { continuation -> - val signer = info.accountSettings.createSigner() - // TODO: Modify the external launcher to launch as different users. - // Right now it only registers if Amber has already approved this signature - if (signer is NostrSignerExternal) { - signer.launcher.registerLauncher( - launcher = { }, - contentResolver = Amethyst.instance::contentResolverFn, - ) - } - + val signer = info.accountSettings.createSigner(Amethyst.instance.contentResolver) RelayAuthEvent.create(info.relays, notificationToken, signer) { result -> continuation.resume(result) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 61623711f3..b919be4d90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -160,7 +160,15 @@ class AccountViewModel( val app: Amethyst, ) : ViewModel(), Dao { - val account = Account(accountSettings, accountSettings.createSigner(), app.locationManager.geohashStateFlow, LocalCache, app.client, viewModelScope) + val account = + Account( + accountSettings, + accountSettings.createSigner(app.contentResolver), + app.locationManager.geohashStateFlow, + LocalCache, + app.client, + viewModelScope, + ) val newNotesPreProcessor = PrecacheNewNotesProcessor(account, LocalCache) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index d4b166713a..c7af1a2d52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -31,10 +31,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LifecycleResumeEffect -import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst @@ -44,8 +41,6 @@ import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.service.relayClient.authCommand.compose.RelayAuthSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountFilterAssemblerSubscription -import com.vitorpamplona.amethyst.ui.MainActivity -import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.AppNavigation import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel @@ -54,7 +49,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.Chat import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription -import com.vitorpamplona.quartz.nip55AndroidSigner.NostrSignerExternal +import com.vitorpamplona.quartz.nip55AndroidSigner.client.IActivityLauncher import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -156,7 +151,10 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) { job?.cancel() job = scope.launch { - PushNotificationUtils.checkAndInit(LocalPreferences.allSavedAccounts(), accountViewModel::okHttpClientForTrustedRelays) + PushNotificationUtils.checkAndInit( + LocalPreferences.allSavedAccounts(), + accountViewModel::okHttpClientForTrustedRelays, + ) } onPauseOrDispose { @@ -167,55 +165,25 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) { @Composable private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) { - if (accountViewModel.account.signer is NostrSignerExternal) { - val activity = getActivity() as MainActivity - - val lifeCycleOwner = LocalLifecycleOwner.current + if (accountViewModel.account.signer is IActivityLauncher) { val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult(), onResult = { result -> - if (result.resultCode != Activity.RESULT_OK) { - accountViewModel.toastManager.toast( - R.string.sign_request_rejected, - R.string.sign_request_rejected_description, - ) - } else { + if (result.resultCode == Activity.RESULT_OK) { result.data?.let { accountViewModel.runOnIO { - accountViewModel.account.signer.launcher - .newResult(it) + accountViewModel.account.signer.newResponse(it) } } } }, ) - DisposableEffect(accountViewModel, accountViewModel.account, launcher, activity, lifeCycleOwner) { - val observer = - LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - accountViewModel.account.signer.launcher.registerLauncher( - launcher = { - try { - launcher.launch(it) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("Signer", "Error opening Signer app", e) - accountViewModel.toastManager.toast( - R.string.error_opening_external_signer, - R.string.error_opening_external_signer_description, - ) - } - }, - contentResolver = Amethyst.instance::contentResolverFn, - ) - } - } - - val launcher: (Intent) -> Unit = { + DisposableEffect(accountViewModel, accountViewModel.account, launcher) { + val launcher: (Intent) -> Unit = { intent -> try { - launcher.launch(it) + launcher.launch(intent) } catch (e: Exception) { if (e is CancellationException) throw e Log.e("Signer", "Error opening Signer app", e) @@ -226,15 +194,9 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) { } } - lifeCycleOwner.lifecycle.addObserver(observer) - accountViewModel.account.signer.launcher.registerLauncher( - launcher = launcher, - contentResolver = Amethyst.instance::contentResolverFn, - ) + accountViewModel.account.signer.registerForegroundLauncher(launcher) onDispose { - accountViewModel.account.signer.launcher - .clearLauncherIf(launcher) - lifeCycleOwner.lifecycle.removeObserver(observer) + accountViewModel.account.signer.unregisterForegroundLauncher(launcher) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt index 783e7557b7..a1210e56a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginOrSignupScreen.kt @@ -27,6 +27,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginPage +import com.vitorpamplona.amethyst.ui.screen.loggedOff.signup.SignUpPage @Composable fun LoginOrSignupScreen( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt deleted file mode 100644 index 4b442a304f..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ /dev/null @@ -1,814 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedOff - -import android.app.Activity -import android.content.Intent -import android.util.Log -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Visibility -import androidx.compose.material.icons.outlined.VisibilityOff -import androidx.compose.material3.Button -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.autofill.AutofillNode -import androidx.compose.ui.autofill.AutofillType -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.boundsInWindow -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalAutofill -import androidx.compose.ui.platform.LocalAutofillTree -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.core.util.Consumer -import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.hashtags.Amethyst -import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons -import com.vitorpamplona.amethyst.service.PackageUtils -import com.vitorpamplona.amethyst.ui.MainActivity -import com.vitorpamplona.amethyst.ui.components.LoadingAnimation -import com.vitorpamplona.amethyst.ui.components.getActivity -import com.vitorpamplona.amethyst.ui.painterRes -import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner -import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.Size0dp -import com.vitorpamplona.amethyst.ui.theme.Size10dp -import com.vitorpamplona.amethyst.ui.theme.Size20dp -import com.vitorpamplona.amethyst.ui.theme.Size35dp -import com.vitorpamplona.amethyst.ui.theme.Size40dp -import com.vitorpamplona.amethyst.ui.theme.Size50dp -import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow -import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.tor.TorSettings -import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser -import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncher -import com.vitorpamplona.quartz.nip55AndroidSigner.SignerType -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import java.util.UUID - -@Preview(device = "spec:width=2160px,height=2340px,dpi=440") -@Composable -fun LoginPage() { - val accountViewModel: AccountStateViewModel = viewModel() - - ThemeComparisonRow( - toPreview = { - LoginPage(accountViewModel, true) {} - }, - ) -} - -@Composable -fun LoginPage( - accountStateViewModel: AccountStateViewModel, - isFirstLogin: Boolean, - newAccountKey: String? = null, - onWantsToLogin: () -> Unit, -) { - val key = remember { mutableStateOf(TextFieldValue(newAccountKey ?: "")) } - var errorMessage by remember { mutableStateOf("") } - val acceptedTerms = remember { mutableStateOf(!isFirstLogin) } - var termsAcceptanceIsRequired by remember { mutableStateOf("") } - - val context = LocalContext.current - val torSettings = remember { mutableStateOf(TorSettings()) } - val isNFCOrQR = remember { mutableStateOf(false) } - val isTemporary = remember { mutableStateOf(false) } - - val scope = rememberCoroutineScope() - var loginWithExternalSigner by remember { mutableStateOf(false) } - - var processingLogin by remember { mutableStateOf(false) } - - val password = remember { mutableStateOf(TextFieldValue("")) } - val needsPassword = - remember { - derivedStateOf { - key.value.text.startsWith("ncryptsec1") - } - } - - val passwordFocusRequester = remember { FocusRequester() } - - if (loginWithExternalSigner) { - PrepareExternalSignerReceiver { pubkey, packageName -> - key.value = TextFieldValue(pubkey) - if (!acceptedTerms.value) { - termsAcceptanceIsRequired = stringRes(context, R.string.acceptance_of_terms_is_required) - } - - if (key.value.text.isBlank()) { - errorMessage = stringRes(context, R.string.key_is_required) - } - - if (acceptedTerms.value && key.value.text.isNotBlank()) { - accountStateViewModel.login( - key = key.value.text, - torSettings = torSettings.value, - transientAccount = isTemporary.value, - loginWithExternalSigner = true, - packageName = packageName, - ) { - errorMessage = stringRes(context, R.string.invalid_key) - } - } - } - } - - Column( - modifier = - Modifier - .fillMaxSize() - .imePadding() - .verticalScroll(rememberScrollState()) - .padding(Size20dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - imageVector = CustomHashTagIcons.Amethyst, - contentDescription = stringRes(R.string.app_logo), - modifier = Modifier.size(150.dp), - contentScale = ContentScale.Inside, - ) - - Spacer(modifier = Modifier.height(Size40dp)) - - KeyTextField( - value = key.value, - onValueChange = { value, isQr -> - key.value = value - if (isQr) { - isNFCOrQR.value = true - isTemporary.value = true - } - if (errorMessage.isNotEmpty()) { - errorMessage = "" - } - }, - ) { - if (!acceptedTerms.value) { - termsAcceptanceIsRequired = stringRes(context, R.string.acceptance_of_terms_is_required) - } - - if (key.value.text.isBlank()) { - errorMessage = stringRes(context, R.string.key_is_required) - } - - if (needsPassword.value && password.value.text.isBlank()) { - errorMessage = stringRes(context, R.string.password_is_required) - } - - if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) { - processingLogin = true - accountStateViewModel.login( - key = key.value.text, - password = password.value.text, - torSettings = torSettings.value, - transientAccount = isTemporary.value, - ) { - processingLogin = false - errorMessage = - if (it != null) { - stringRes(context, R.string.invalid_key_with_message, it) - } else { - stringRes(context, R.string.invalid_key) - } - } - } - } - - if (errorMessage.isNotBlank()) { - Text( - text = errorMessage, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - ) - } - - Spacer(modifier = Modifier.height(10.dp)) - - if (needsPassword.value) { - PasswordField( - value = password.value, - onValueChange = { - password.value = it - if (errorMessage.isNotEmpty()) { - errorMessage = "" - } - }, - passwordFocusRequester, - ) { - if (!acceptedTerms.value) { - termsAcceptanceIsRequired = stringRes(context, R.string.acceptance_of_terms_is_required) - } - - if (key.value.text.isBlank()) { - errorMessage = stringRes(context, R.string.key_is_required) - } - - if (needsPassword.value && password.value.text.isBlank()) { - errorMessage = stringRes(context, R.string.password_is_required) - } - - if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) { - processingLogin = true - accountStateViewModel.login(key.value.text, password.value.text, torSettings.value, isTemporary.value) { - processingLogin = false - errorMessage = - if (it != null) { - stringRes(context, R.string.invalid_key_with_message, it) - } else { - stringRes(context, R.string.invalid_key) - } - } - } - } - } - Spacer(modifier = Modifier.height(10.dp)) - - TorSettingsSetup( - torSettings = torSettings.value, - onCheckedChange = { - torSettings.value = it - }, - onError = { - scope.launch { - Toast - .makeText( - context, - it, - Toast.LENGTH_LONG, - ).show() - } - }, - ) - - if (isNFCOrQR.value) { - OfferTemporaryAccount( - checked = isTemporary.value, - onCheckedChange = { isTemporary.value = it }, - ) - } - - if (isFirstLogin) { - AcceptTerms( - checked = acceptedTerms.value, - onCheckedChange = { acceptedTerms.value = it }, - ) - - if (termsAcceptanceIsRequired.isNotBlank()) { - Text( - text = termsAcceptanceIsRequired, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - ) - } - } - - Spacer(modifier = Modifier.height(Size10dp)) - - Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) { - LoginButton( - enabled = acceptedTerms.value, - processingLogin = processingLogin, - onClick = { - if (!acceptedTerms.value) { - termsAcceptanceIsRequired = - stringRes(context, R.string.acceptance_of_terms_is_required) - } - - if (key.value.text.isBlank()) { - errorMessage = stringRes(context, R.string.key_is_required) - } - - if (needsPassword.value && password.value.text.isBlank()) { - errorMessage = stringRes(context, R.string.password_is_required) - } - - if (acceptedTerms.value && key.value.text.isNotBlank() && !(needsPassword.value && password.value.text.isBlank())) { - processingLogin = true - accountStateViewModel.login(key.value.text, password.value.text, torSettings.value, isTemporary.value) { - processingLogin = false - errorMessage = - if (it != null) { - stringRes(context, R.string.invalid_key_with_message, it) - } else { - stringRes(context, R.string.invalid_key) - } - } - } - }, - ) - } - - if (PackageUtils.isExternalSignerInstalled(context)) { - Box(modifier = Modifier.padding(Size40dp, Size20dp, Size40dp, Size0dp)) { - LoginWithAmberButton( - enabled = acceptedTerms.value, - onClick = { - if (!acceptedTerms.value) { - termsAcceptanceIsRequired = stringRes(context, R.string.acceptance_of_terms_is_required) - } else { - loginWithExternalSigner = true - } - }, - ) - } - } - - Spacer(modifier = Modifier.height(Size40dp)) - - Text(text = stringRes(R.string.don_t_have_an_account)) - - Spacer(modifier = Modifier.height(Size20dp)) - - Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) { - SignUpButton(onWantsToLogin) - } - } - - OpenURIIfNotLoggedIn { - key.value = TextFieldValue(it) - acceptedTerms.value = true - isNFCOrQR.value = true - isTemporary.value = true - if (it.startsWith("ncryptsec1")) { - delay(300) - passwordFocusRequester.requestFocus() - } - } -} - -@Composable -fun OfferTemporaryAccount( - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, -) { - Row(verticalAlignment = Alignment.CenterVertically) { - Checkbox( - checked = checked, - onCheckedChange = onCheckedChange, - ) - - Text(stringRes(R.string.temporary_account)) - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun PasswordField( - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, - passwordFocusRequester: FocusRequester, - onGo: () -> Unit, -) { - val autofillNodeKey = - AutofillNode( - autofillTypes = listOf(AutofillType.Password), - onFill = { onValueChange(TextFieldValue(it)) }, - ) - - val autofillNodePassword = - AutofillNode( - autofillTypes = listOf(AutofillType.Password), - onFill = { onValueChange(TextFieldValue(it)) }, - ) - - val autofill = LocalAutofill.current - LocalAutofillTree.current += autofillNodeKey - LocalAutofillTree.current += autofillNodePassword - - var showCharsPassword by remember { mutableStateOf(false) } - OutlinedTextField( - modifier = - Modifier - .focusRequester(passwordFocusRequester) - .onGloballyPositioned { coordinates -> - autofillNodePassword.boundingBox = coordinates.boundsInWindow() - }.onFocusChanged { focusState -> - autofill?.run { - if (focusState.isFocused) { - requestAutofillForNode(autofillNodePassword) - } else { - cancelAutofillForNode(autofillNodePassword) - } - } - }, - value = value, - onValueChange = onValueChange, - keyboardOptions = - KeyboardOptions( - autoCorrectEnabled = false, - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Go, - ), - placeholder = { - Text( - text = stringRes(R.string.ncryptsec_password), - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - trailingIcon = { - Row { - IconButton(onClick = { showCharsPassword = !showCharsPassword }) { - Icon( - imageVector = - if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, - contentDescription = - if (showCharsPassword) { - stringRes(R.string.show_password) - } else { - stringRes( - R.string.hide_password, - ) - }, - ) - } - } - }, - visualTransformation = - if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(), - keyboardActions = - KeyboardActions( - onGo = { - onGo() - }, - ), - ) -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun KeyTextField( - value: TextFieldValue, - onValueChange: (TextFieldValue, throughQR: Boolean) -> Unit, - onLogin: () -> Unit, -) { - var dialogOpen by remember { mutableStateOf(false) } - - var showCharsKey by remember { mutableStateOf(false) } - - val autofillNodeKey = - AutofillNode( - autofillTypes = listOf(AutofillType.Password), - onFill = { onValueChange(TextFieldValue(it), false) }, - ) - - val autofillNodePassword = - AutofillNode( - autofillTypes = listOf(AutofillType.Password), - onFill = { onValueChange(TextFieldValue(it), false) }, - ) - - val autofill = LocalAutofill.current - LocalAutofillTree.current += autofillNodeKey - LocalAutofillTree.current += autofillNodePassword - - OutlinedTextField( - modifier = - Modifier - .onGloballyPositioned { coordinates -> - autofillNodeKey.boundingBox = coordinates.boundsInWindow() - }.onFocusChanged { focusState -> - autofill?.run { - if (focusState.isFocused) { - requestAutofillForNode(autofillNodeKey) - } else { - cancelAutofillForNode(autofillNodeKey) - } - } - }, - value = value, - onValueChange = { onValueChange(it, false) }, - keyboardOptions = - KeyboardOptions( - autoCorrectEnabled = false, - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Go, - ), - placeholder = { - Text( - text = stringRes(R.string.nsec_npub_hex_private_key), - color = MaterialTheme.colorScheme.placeholderText, - ) - }, - trailingIcon = { - Row { - IconButton(onClick = { showCharsKey = !showCharsKey }) { - Icon( - imageVector = - if (showCharsKey) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, - contentDescription = - if (showCharsKey) { - stringRes(R.string.show_password) - } else { - stringRes( - R.string.hide_password, - ) - }, - ) - } - } - }, - leadingIcon = { - if (dialogOpen) { - SimpleQrCodeScanner { - dialogOpen = false - if (!it.isNullOrEmpty()) { - onValueChange(TextFieldValue(it), true) - } - } - } - IconButton(onClick = { dialogOpen = true }) { - Icon( - painter = painterRes(R.drawable.ic_qrcode, 5), - contentDescription = - stringRes( - R.string.login_with_qr_code, - ), - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary, - ) - } - }, - visualTransformation = - if (showCharsKey) VisualTransformation.None else PasswordVisualTransformation(), - keyboardActions = - KeyboardActions( - onGo = { - onLogin() - }, - ), - ) -} - -@Composable -private fun PrepareExternalSignerReceiver(onLogin: (pubkey: String, packageName: String) -> Unit) { - val scope = rememberCoroutineScope() - val externalSignerLauncher = remember { ExternalSignerLauncher("", signerPackageName = "") } - val id = remember { UUID.randomUUID().toString() } - - val launcher = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult(), - onResult = { result -> - if (result.resultCode != Activity.RESULT_OK) { - scope.launch(Dispatchers.Main) { - Toast - .makeText( - Amethyst.instance, - "Sign request rejected", - Toast.LENGTH_SHORT, - ).show() - } - } else { - result.data?.let { externalSignerLauncher.newResult(it) } - } - }, - ) - - val activity = getActivity() as MainActivity - - DisposableEffect(launcher, activity, externalSignerLauncher) { - val launcher: (Intent) -> Unit = { - try { - launcher.launch(it) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("Signer", "Error opening Signer app", e) - scope.launch(Dispatchers.Main) { - Toast - .makeText( - Amethyst.instance, - R.string.error_opening_external_signer, - Toast.LENGTH_SHORT, - ).show() - } - } - } - - externalSignerLauncher.registerLauncher( - launcher = launcher, - contentResolver = Amethyst.instance::contentResolverFn, - ) - onDispose { - externalSignerLauncher.clearLauncherIf(launcher) - } - } - - LaunchedEffect(externalSignerLauncher) { - externalSignerLauncher.openSignerApp( - "", - SignerType.GET_PUBLIC_KEY, - "", - id, - ) { result -> - val split = result.split("-") - val pubkey = split.first() - val packageName = if (split.size > 1) split[1] else "" - - onLogin(pubkey, packageName) - } - } -} - -@Composable -private fun OpenURIIfNotLoggedIn(onNewNIP19: suspend (String) -> Unit) { - val context = LocalContext.current - val activity = context.getActivity() - val scope = rememberCoroutineScope() - - var currentIntentNextPage by remember { - val uri = - activity.intent - ?.data - ?.toString() - ?.ifBlank { null } - - activity.intent.data = null - - mutableStateOf(uri) - } - - currentIntentNextPage?.let { intentNextPage -> - var nip19 by remember { - mutableStateOf( - Nip19Parser.tryParseAndClean(currentIntentNextPage), - ) - } - - LaunchedEffect(intentNextPage) { - if (nip19 != null) { - nip19?.let { - scope.launch { - onNewNIP19(it) - } - nip19 = null - } - } else { - scope.launch { - Toast - .makeText( - context, - stringRes(context, R.string.invalid_nip19_uri_description, intentNextPage), - Toast.LENGTH_SHORT, - ).show() - } - } - - currentIntentNextPage = null - } - } - - DisposableEffect(activity) { - val consumer = - Consumer { intent -> - val uri = intent.data?.toString() - if (!uri.isNullOrBlank()) { - val newNip19 = Nip19Parser.tryParseAndClean(uri) - if (newNip19 != null) { - scope.launch { - onNewNIP19(newNip19) - } - } else { - scope.launch { - delay(1000) - Toast - .makeText( - context, - stringRes(context, R.string.invalid_nip19_uri_description, uri), - Toast.LENGTH_SHORT, - ).show() - } - } - } - } - activity.addOnNewIntentListener(consumer) - onDispose { activity.removeOnNewIntentListener(consumer) } - } -} - -@Composable -fun SignUpButton(onClick: () -> Unit) { - OutlinedButton( - onClick = onClick, - shape = RoundedCornerShape(Size35dp), - modifier = Modifier.height(50.dp), - ) { - Text( - text = stringRes(R.string.sign_up), - modifier = Modifier.padding(horizontal = Size40dp), - ) - } -} - -@Composable -fun LoginWithAmberButton( - enabled: Boolean, - onClick: () -> Unit, -) { - Button( - enabled = enabled, - onClick = onClick, - shape = RoundedCornerShape(Size35dp), - modifier = Modifier.height(Size50dp), - ) { - Text( - text = stringRes(R.string.login_with_external_signer), - modifier = Modifier.padding(horizontal = Size40dp), - ) - } -} - -@Composable -fun LoginButton( - enabled: Boolean, - processingLogin: Boolean, - onClick: () -> Unit, -) { - Button( - enabled = enabled, - onClick = onClick, - shape = RoundedCornerShape(Size35dp), - modifier = Modifier.height(Size50dp), - ) { - Row(modifier = Modifier.padding(horizontal = Size40dp)) { - if (processingLogin) { - LoadingAnimation() - Spacer(modifier = DoubleHorzSpacer) - } - Text(stringRes(R.string.login)) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt index 43051007bb..9e3ca242b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt @@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.ui.components.appendLink import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog import com.vitorpamplona.amethyst.ui.tor.TorSettings -import com.vitorpamplona.amethyst.ui.tor.TorType @Composable fun TorSettingsSetup( @@ -45,8 +44,6 @@ fun TorSettingsSetup( onError: (String) -> Unit, ) { var connectOrbotDialogOpen by remember { mutableStateOf(false) } - var activeTor by remember { mutableStateOf(false) } - val primary = MaterialTheme.colorScheme.primary Text( @@ -63,7 +60,6 @@ fun TorSettingsSetup( torSettings = torSettings, onClose = { connectOrbotDialogOpen = false }, onPost = { torSettings -> - activeTor = torSettings.torType != TorType.OFF connectOrbotDialogOpen = false onCheckedChange(torSettings) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt new file mode 100644 index 0000000000..309065eadd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.login + +import android.app.Activity +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.TextFieldValue +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.DefaultSignerPermissions +import com.vitorpamplona.amethyst.ui.theme.Size0dp +import com.vitorpamplona.amethyst.ui.theme.Size20dp +import com.vitorpamplona.amethyst.ui.theme.Size40dp +import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.client.ExternalSignerLogin +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.launch + +@Composable +fun ExternalSignerButton(loginViewModel: LoginViewModel) { + val scope = rememberCoroutineScope() + + val launcher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + ) { result -> + scope.launch { + val resultData = result.data + if (result.resultCode == Activity.RESULT_OK && resultData != null) { + val loginInfo = ExternalSignerLogin.parseResult(resultData) + if (loginInfo is SignerResult.Successful) { + loginViewModel.updateKey(TextFieldValue(loginInfo.result.pubkey), false) + loginViewModel.loginWithExternalSigner(loginInfo.result.packageName) + } + } else { + loginViewModel.errorManager.error(R.string.sign_request_rejected2) + } + } + } + + Box(modifier = Modifier.padding(Size40dp, Size20dp, Size40dp, Size0dp)) { + LoginWithAmberButton( + enabled = loginViewModel.acceptedTerms, + onClick = { + if (!loginViewModel.acceptedTerms) { + loginViewModel.termsAcceptanceIsRequiredError = true + } else { + try { + launcher.launch(ExternalSignerLogin.createIntent(DefaultSignerPermissions)) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("Signer", "Error opening Signer app", e) + loginViewModel.errorManager.error(R.string.error_opening_external_signer) + } + } + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextField.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextField.kt new file mode 100644 index 0000000000..d0e73a6a64 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/KeyTextField.kt @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.login + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.AutofillNode +import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalAutofill +import androidx.compose.ui.platform.LocalAutofillTree +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.painterRes +import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.SimpleQrCodeScanner +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun KeyTextField( + value: TextFieldValue, + onValueChange: (TextFieldValue, throughQR: Boolean) -> Unit, + onLogin: () -> Unit, +) { + var dialogOpen by remember { mutableStateOf(false) } + + var showCharsKey by remember { mutableStateOf(false) } + + val autofillNodeKey = + AutofillNode( + autofillTypes = listOf(AutofillType.Password), + onFill = { onValueChange(TextFieldValue(it), false) }, + ) + + val autofillNodePassword = + AutofillNode( + autofillTypes = listOf(AutofillType.Password), + onFill = { onValueChange(TextFieldValue(it), false) }, + ) + + val autofill = LocalAutofill.current + LocalAutofillTree.current += autofillNodeKey + LocalAutofillTree.current += autofillNodePassword + + OutlinedTextField( + modifier = + Modifier + .onGloballyPositioned { coordinates -> + autofillNodeKey.boundingBox = coordinates.boundsInWindow() + }.onFocusChanged { focusState -> + autofill?.run { + if (focusState.isFocused) { + requestAutofillForNode(autofillNodeKey) + } else { + cancelAutofillForNode(autofillNodeKey) + } + } + }, + value = value, + onValueChange = { onValueChange(it, false) }, + keyboardOptions = + KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go, + ), + placeholder = { + Text( + text = stringRes(R.string.nsec_npub_hex_private_key), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + trailingIcon = { + Row { + IconButton(onClick = { showCharsKey = !showCharsKey }) { + Icon( + imageVector = + if (showCharsKey) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = + if (showCharsKey) { + stringRes(R.string.show_password) + } else { + stringRes( + R.string.hide_password, + ) + }, + ) + } + } + }, + leadingIcon = { + if (dialogOpen) { + SimpleQrCodeScanner { + dialogOpen = false + if (!it.isNullOrEmpty()) { + onValueChange(TextFieldValue(it), true) + } + } + } + IconButton(onClick = { dialogOpen = true }) { + Icon( + painter = painterRes(R.drawable.ic_qrcode, 5), + contentDescription = stringRes(R.string.login_with_qr_code), + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.primary, + ) + } + }, + visualTransformation = + if (showCharsKey) VisualTransformation.None else PasswordVisualTransformation(), + keyboardActions = + KeyboardActions( + onGo = { + onLogin() + }, + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButton.kt new file mode 100644 index 0000000000..642ece51a5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginButton.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.login + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.components.LoadingAnimation +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.Size40dp +import com.vitorpamplona.amethyst.ui.theme.Size50dp + +@Composable +fun LoginButton( + enabled: Boolean, + processingLogin: Boolean, + onClick: () -> Unit, +) { + Button( + enabled = enabled, + onClick = onClick, + shape = RoundedCornerShape(Size35dp), + modifier = Modifier.height(Size50dp), + ) { + Row(modifier = Modifier.padding(horizontal = Size40dp)) { + if (processingLogin) { + LoadingAnimation() + Spacer(modifier = DoubleHorzSpacer) + } + Text(stringRes(R.string.login)) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager.kt new file mode 100644 index 0000000000..9dc57b9d37 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginErrorManager.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.login + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +class LoginErrorManager { + interface IErrorMsg + + class SingleErrorMsg( + val errorResId: Int, + ) : IErrorMsg + + class ParamsErrorMsg( + val errorResId: Int, + val params: Array, + ) : IErrorMsg + + var error by mutableStateOf(null) + + fun clearErrors() { + error = null + } + + fun error(resourceId: Int) { + error = SingleErrorMsg(resourceId) + } + + fun error( + resourceId: Int, + vararg params: String, + ) { + error = ParamsErrorMsg(resourceId, params) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt new file mode 100644 index 0000000000..8329b0b94c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginScreen.kt @@ -0,0 +1,362 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.login + +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.AutofillNode +import androidx.compose.ui.autofill.AutofillType +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalAutofill +import androidx.compose.ui.platform.LocalAutofillTree +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.hashtags.Amethyst +import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedOff.AcceptTerms +import com.vitorpamplona.amethyst.ui.screen.loggedOff.TorSettingsSetup +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size20dp +import com.vitorpamplona.amethyst.ui.theme.Size40dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip55AndroidSigner.client.isExternalSignerInstalled +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Preview(device = "spec:width=2160px,height=2340px,dpi=440") +@Composable +fun LoginPagePreview() { + val accountViewModel: AccountStateViewModel = viewModel() + + ThemeComparisonRow( + toPreview = { + LoginPage(accountViewModel, true) {} + }, + ) +} + +@Composable +fun LoginPage( + accountStateViewModel: AccountStateViewModel, + isFirstLogin: Boolean, + newAccountKey: String? = null, + onWantsToLogin: () -> Unit, +) { + val loginViewModel: LoginViewModel = viewModel() + loginViewModel.init(accountStateViewModel) + + LaunchedEffect(Unit) { + loginViewModel.load(isFirstLogin, newAccountKey) + } + + LoginPage(loginViewModel, onWantsToLogin) +} + +@Composable +fun LoginPage( + loginViewModel: LoginViewModel, + onWantsToLogin: () -> Unit, +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + + Column( + modifier = + Modifier + .fillMaxSize() + .imePadding() + .verticalScroll(rememberScrollState()) + .padding(Size20dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + imageVector = CustomHashTagIcons.Amethyst, + contentDescription = stringRes(R.string.app_logo), + modifier = Modifier.size(150.dp), + contentScale = ContentScale.Inside, + ) + + Spacer(modifier = Modifier.height(Size40dp)) + + KeyTextField( + value = loginViewModel.key, + onValueChange = loginViewModel::updateKey, + onLogin = loginViewModel::login, + ) + + loginViewModel.errorManager.error?.let { error -> + when (error) { + is LoginErrorManager.SingleErrorMsg -> + Text( + text = stringRes(error.errorResId), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + is LoginErrorManager.ParamsErrorMsg -> + Text( + text = stringRes(error.errorResId, *error.params), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + else -> {} + } + } + + Spacer(modifier = Modifier.height(10.dp)) + + PasswordField(loginViewModel) + + Spacer(modifier = Modifier.height(10.dp)) + + TorSettingsSetup( + torSettings = loginViewModel.torSettings, + onCheckedChange = loginViewModel::updateTorSettings, + onError = { + scope.launch { + Toast + .makeText( + context, + it, + Toast.LENGTH_LONG, + ).show() + } + }, + ) + + if (loginViewModel.offerTemporaryLogin) { + OfferTemporaryAccount( + checked = loginViewModel.isTemporary, + onCheckedChange = { loginViewModel.isTemporary = it }, + ) + } + + if (loginViewModel.isFirstLogin) { + AcceptTerms( + checked = loginViewModel.acceptedTerms, + onCheckedChange = loginViewModel::updateAcceptedTerms, + ) + + if (loginViewModel.termsAcceptanceIsRequiredError) { + Text( + text = stringRes(R.string.acceptance_of_terms_is_required), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + } + } + + Spacer(modifier = Modifier.height(Size10dp)) + + Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) { + LoginButton( + enabled = loginViewModel.acceptedTerms, + processingLogin = loginViewModel.processingLogin, + onClick = loginViewModel::login, + ) + } + + if (isExternalSignerInstalled(context)) { + ExternalSignerButton(loginViewModel) + } + + Spacer(modifier = Modifier.height(Size40dp)) + + Text(text = stringRes(R.string.don_t_have_an_account)) + + Spacer(modifier = Modifier.height(Size20dp)) + + Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) { + SignUpButton(onWantsToLogin) + } + } + + OpenURIIfNotLoggedIn { key -> + loginViewModel.updateKey(TextFieldValue(key), true) + loginViewModel.updateOfferTemporaryLogin(true) + } +} + +@Composable +fun OfferTemporaryAccount( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange, + ) + + Text(stringRes(R.string.temporary_account)) + } +} + +@Composable +private fun PasswordField(loginViewModel: LoginViewModel) { + if (loginViewModel.needsPassword) { + val passwordFocusRequester = remember { FocusRequester() } + + PasswordField( + value = loginViewModel.password, + onValueChange = loginViewModel::updatePassword, + passwordFocusRequester = passwordFocusRequester, + onGo = loginViewModel::login, + ) + + LaunchedEffect(Unit) { + delay(300) + passwordFocusRequester.requestFocus() + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun PasswordField( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + passwordFocusRequester: FocusRequester, + onGo: () -> Unit, +) { + val autofillNodeKey = + AutofillNode( + autofillTypes = listOf(AutofillType.Password), + onFill = { onValueChange(TextFieldValue(it)) }, + ) + + val autofillNodePassword = + AutofillNode( + autofillTypes = listOf(AutofillType.Password), + onFill = { onValueChange(TextFieldValue(it)) }, + ) + + val autofill = LocalAutofill.current + LocalAutofillTree.current += autofillNodeKey + LocalAutofillTree.current += autofillNodePassword + + var showCharsPassword by remember { mutableStateOf(false) } + OutlinedTextField( + modifier = + Modifier + .focusRequester(passwordFocusRequester) + .onGloballyPositioned { coordinates -> + autofillNodePassword.boundingBox = coordinates.boundsInWindow() + }.onFocusChanged { focusState -> + autofill?.run { + if (focusState.isFocused) { + requestAutofillForNode(autofillNodePassword) + } else { + cancelAutofillForNode(autofillNodePassword) + } + } + }, + value = value, + onValueChange = onValueChange, + keyboardOptions = + KeyboardOptions( + autoCorrectEnabled = false, + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go, + ), + placeholder = { + Text( + text = stringRes(R.string.ncryptsec_password), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + trailingIcon = { + Row { + IconButton(onClick = { showCharsPassword = !showCharsPassword }) { + Icon( + imageVector = + if (showCharsPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, + contentDescription = + if (showCharsPassword) { + stringRes(R.string.show_password) + } else { + stringRes( + R.string.hide_password, + ) + }, + ) + } + } + }, + visualTransformation = + if (showCharsPassword) VisualTransformation.None else PasswordVisualTransformation(), + keyboardActions = + KeyboardActions( + onGo = { + onGo() + }, + ), + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt new file mode 100644 index 0000000000..b113f15159 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.login + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.tor.TorSettings + +class LoginViewModel : ViewModel() { + lateinit var accountStateViewModel: AccountStateViewModel + + val errorManager = LoginErrorManager() + + var key by mutableStateOf(TextFieldValue("")) + var acceptedTerms by mutableStateOf(false) + var termsAcceptanceIsRequiredError by mutableStateOf(false) + + var torSettings by mutableStateOf(TorSettings()) + var offerTemporaryLogin by mutableStateOf(false) + var isTemporary by mutableStateOf(false) + + var processingLogin by mutableStateOf(false) + + var password by mutableStateOf(TextFieldValue("")) + val needsPassword by derivedStateOf { + key.text.startsWith("ncryptsec1") + } + + var isFirstLogin by mutableStateOf(false) + + fun init(accountStateViewModel: AccountStateViewModel) { + this.accountStateViewModel = accountStateViewModel + } + + fun load( + isFirstLogin: Boolean, + newAccountKey: String?, + ) { + this.isFirstLogin = isFirstLogin + acceptedTerms = isFirstLogin + if (newAccountKey != null) { + key = TextFieldValue(newAccountKey) + } + } + + fun updateKey( + value: TextFieldValue, + throughQR: Boolean, + ) { + key = value + if (throughQR) { + offerTemporaryLogin = true + isTemporary = true + } + errorManager.clearErrors() + } + + fun updatePassword(newPassword: TextFieldValue) { + password = newPassword + errorManager.clearErrors() + } + + fun updateTorSettings(newTorSettings: TorSettings) { + torSettings = newTorSettings + } + + fun updateAcceptedTerms(newAcceptedTerms: Boolean) { + acceptedTerms = newAcceptedTerms + if (newAcceptedTerms) { + termsAcceptanceIsRequiredError = false + } + + errorManager.clearErrors() + } + + fun updateOfferTemporaryLogin(tempLogin: Boolean) { + offerTemporaryLogin = tempLogin + } + + fun checkCanLogin(): Boolean { + if (!acceptedTerms) { + termsAcceptanceIsRequiredError = true + return false + } + + if (key.text.isBlank()) { + errorManager.error(R.string.key_is_required) + return false + } + + if (needsPassword && password.text.isBlank()) { + errorManager.error(R.string.password_is_required) + return false + } + + return true + } + + fun login() { + if (checkCanLogin()) { + processingLogin = true + accountStateViewModel.login( + key = key.text, + password = password.text, + torSettings = torSettings, + transientAccount = isTemporary, + ) { + processingLogin = false + if (it != null) { + errorManager.error(R.string.invalid_key_with_message, it) + } else { + errorManager.error(R.string.invalid_key) + } + } + } + } + + fun loginWithExternalSigner(packageName: String) { + if (checkCanLogin()) { + processingLogin = true + accountStateViewModel.login( + key = key.text, + torSettings = torSettings, + transientAccount = isTemporary, + loginWithExternalSigner = true, + packageName = packageName, + ) { + processingLogin = false + errorManager.error(R.string.sign_request_rejected_description) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginWithAmberButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginWithAmberButton.kt new file mode 100644 index 0000000000..d293ac7606 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginWithAmberButton.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.login + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.Size40dp +import com.vitorpamplona.amethyst.ui.theme.Size50dp + +@Composable +fun LoginWithAmberButton( + enabled: Boolean, + onClick: () -> Unit, +) { + Button( + enabled = enabled, + onClick = onClick, + shape = RoundedCornerShape(Size35dp), + modifier = Modifier.height(Size50dp), + ) { + Text( + text = stringRes(R.string.login_with_external_signer), + modifier = Modifier.padding(horizontal = Size40dp), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedIn.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedIn.kt new file mode 100644 index 0000000000..856306819c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/OpenURIIfNotLoggedIn.kt @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.login + +import android.content.Intent +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.core.util.Consumer +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.components.getActivity +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun OpenURIIfNotLoggedIn(onNewNIP19: suspend (String) -> Unit) { + val context = LocalContext.current + val activity = context.getActivity() + val scope = rememberCoroutineScope() + + var currentIntentNextPage by remember { + val uri = + activity.intent + ?.data + ?.toString() + ?.ifBlank { null } + + activity.intent.data = null + + mutableStateOf(uri) + } + + currentIntentNextPage?.let { intentNextPage -> + var nip19 by remember { + mutableStateOf( + Nip19Parser.tryParseAndClean(currentIntentNextPage), + ) + } + + LaunchedEffect(intentNextPage) { + if (nip19 != null) { + nip19?.let { + scope.launch { + onNewNIP19(it) + } + nip19 = null + } + } else { + scope.launch { + Toast + .makeText( + context, + stringRes(context, R.string.invalid_nip19_uri_description, intentNextPage), + Toast.LENGTH_SHORT, + ).show() + } + } + + currentIntentNextPage = null + } + } + + DisposableEffect(activity) { + val consumer = + Consumer { intent -> + val uri = intent.data?.toString() + if (!uri.isNullOrBlank()) { + val newNip19 = Nip19Parser.tryParseAndClean(uri) + if (newNip19 != null) { + scope.launch { + onNewNIP19(newNip19) + } + } else { + scope.launch { + delay(1000) + Toast + .makeText( + context, + stringRes(context, R.string.invalid_nip19_uri_description, uri), + Toast.LENGTH_SHORT, + ).show() + } + } + } + } + activity.addOnNewIntentListener(consumer) + onDispose { activity.removeOnNewIntentListener(consumer) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/SignUpButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/SignUpButton.kt new file mode 100644 index 0000000000..140d5865e5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/SignUpButton.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.login + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.Size40dp + +@Composable +fun SignUpButton(onClick: () -> Unit) { + OutlinedButton( + onClick = onClick, + shape = RoundedCornerShape(Size35dp), + modifier = Modifier.height(50.dp), + ) { + Text( + text = stringRes(R.string.sign_up), + modifier = Modifier.padding(horizontal = Size40dp), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/LoginButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/LoginButton.kt new file mode 100644 index 0000000000..7991bf9ba5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/LoginButton.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.signup + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.Size40dp + +@Composable +fun LoginButton(onWantsToLogin: () -> Unit) { + OutlinedButton( + onClick = onWantsToLogin, + shape = RoundedCornerShape(Size35dp), + modifier = Modifier.height(50.dp), + ) { + Text( + text = stringRes(R.string.login), + modifier = Modifier.padding(horizontal = Size40dp), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpButton.kt new file mode 100644 index 0000000000..64076886ea --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpButton.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.signup + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.Size40dp + +@Composable +fun SignUpButton( + enabled: Boolean, + onClick: () -> Unit, +) { + Button( + enabled = enabled, + onClick = onClick, + shape = RoundedCornerShape(Size35dp), + modifier = Modifier.height(50.dp), + ) { + Text( + text = stringRes(R.string.create_account), + modifier = Modifier.padding(horizontal = Size40dp), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt similarity index 64% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt index d2a4478b2a..ca7c3e8045 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/SignUpScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedOff +package com.vitorpamplona.amethyst.ui.screen.loggedOff.signup import android.widget.Toast import androidx.compose.foundation.Image @@ -32,19 +32,14 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -53,7 +48,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel @@ -61,19 +55,20 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.hashtags.Amethyst import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedOff.AcceptTerms +import com.vitorpamplona.amethyst.ui.screen.loggedOff.TorSettingsSetup +import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20dp -import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.tor.TorSettings import kotlinx.coroutines.launch @Preview(device = "spec:width=2160px,height=2340px,dpi=440") @Composable -fun SignUpPage() { +fun SignUpPagePreview() { val accountViewModel: AccountStateViewModel = viewModel() ThemeComparisonRow( @@ -88,13 +83,18 @@ fun SignUpPage( accountStateViewModel: AccountStateViewModel, onWantsToLogin: () -> Unit, ) { - val displayName = remember { mutableStateOf(TextFieldValue("")) } - var errorMessage by remember { mutableStateOf("") } - val acceptedTerms = remember { mutableStateOf(false) } - var termsAcceptanceIsRequired by remember { mutableStateOf("") } + val signUpViewModel: SignUpViewModel = viewModel() + signUpViewModel.init(accountStateViewModel) + SignUpPage(signUpViewModel, onWantsToLogin) +} + +@Composable +fun SignUpPage( + signUpViewModel: SignUpViewModel, + onWantsToLogin: () -> Unit, +) { val context = LocalContext.current - val torSettings = remember { mutableStateOf(TorSettings()) } val scope = rememberCoroutineScope() Column( @@ -125,8 +125,8 @@ fun SignUpPage( Spacer(modifier = Modifier.height(Size20dp)) OutlinedTextField( - value = displayName.value, - onValueChange = { displayName.value = it }, + value = signUpViewModel.displayName, + onValueChange = signUpViewModel::updateDisplayName, keyboardOptions = KeyboardOptions( autoCorrectEnabled = false, @@ -142,49 +142,47 @@ fun SignUpPage( keyboardActions = KeyboardActions( onGo = { - if (!acceptedTerms.value) { - termsAcceptanceIsRequired = - stringRes(context, R.string.acceptance_of_terms_is_required) - } - - if (displayName.value.text.isBlank()) { - errorMessage = stringRes(context, R.string.name_is_required) - } - - if (acceptedTerms.value && displayName.value.text.isNotBlank()) { - accountStateViewModel.newKey(torSettings.value, displayName.value.text) - } + signUpViewModel.signup() }, ), ) - if (errorMessage.isNotBlank()) { - Text( - text = errorMessage, - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodySmall, - ) + + signUpViewModel.errorManager.error?.let { error -> + when (error) { + is LoginErrorManager.SingleErrorMsg -> + Text( + text = stringRes(error.errorResId), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + is LoginErrorManager.ParamsErrorMsg -> + Text( + text = stringRes(error.errorResId, *error.params), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + else -> {} + } } Spacer(modifier = Modifier.height(10.dp)) AcceptTerms( - checked = acceptedTerms.value, - onCheckedChange = { acceptedTerms.value = it }, + checked = signUpViewModel.acceptedTerms, + onCheckedChange = signUpViewModel::updateAcceptedTerms, ) - if (termsAcceptanceIsRequired.isNotBlank()) { + if (signUpViewModel.termsAcceptanceIsRequiredError) { Text( - text = termsAcceptanceIsRequired, + text = stringRes(R.string.acceptance_of_terms_is_required), color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall, ) } TorSettingsSetup( - torSettings = torSettings.value, - onCheckedChange = { - torSettings.value = it - }, + torSettings = signUpViewModel.torSettings, + onCheckedChange = signUpViewModel::updateTorSettings, onError = { scope.launch { Toast @@ -201,20 +199,8 @@ fun SignUpPage( Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) { SignUpButton( - enabled = acceptedTerms.value, - onClick = { - if (!acceptedTerms.value) { - termsAcceptanceIsRequired = stringRes(context, R.string.acceptance_of_terms_is_required) - } - - if (displayName.value.text.isBlank()) { - errorMessage = stringRes(context, R.string.name_is_required) - } - - if (acceptedTerms.value && displayName.value.text.isNotBlank()) { - accountStateViewModel.newKey(torSettings.value, displayName.value.text) - } - }, + enabled = signUpViewModel.acceptedTerms, + onClick = signUpViewModel::signup, ) } @@ -229,35 +215,3 @@ fun SignUpPage( } } } - -@Composable -fun LoginButton(onWantsToLogin: () -> Unit) { - OutlinedButton( - onClick = onWantsToLogin, - shape = RoundedCornerShape(Size35dp), - modifier = Modifier.height(50.dp), - ) { - Text( - text = stringRes(R.string.login), - modifier = Modifier.padding(horizontal = Size40dp), - ) - } -} - -@Composable -fun SignUpButton( - enabled: Boolean, - onClick: () -> Unit, -) { - Button( - enabled = enabled, - onClick = onClick, - shape = RoundedCornerShape(Size35dp), - modifier = Modifier.height(50.dp), - ) { - Text( - text = stringRes(R.string.create_account), - modifier = Modifier.padding(horizontal = Size40dp), - ) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt new file mode 100644 index 0000000000..39f8500a35 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpViewModel.kt @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedOff.signup + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedOff.login.LoginErrorManager +import com.vitorpamplona.amethyst.ui.tor.TorSettings + +class SignUpViewModel : ViewModel() { + lateinit var accountStateViewModel: AccountStateViewModel + + val errorManager = LoginErrorManager() + + var displayName by mutableStateOf(TextFieldValue("")) + + var acceptedTerms by mutableStateOf(false) + var termsAcceptanceIsRequiredError by mutableStateOf(false) + + var torSettings by mutableStateOf(TorSettings()) + + fun init(accountStateViewModel: AccountStateViewModel) { + this.accountStateViewModel = accountStateViewModel + } + + fun updateDisplayName(value: TextFieldValue) { + displayName = value + errorManager.clearErrors() + } + + fun updateTorSettings(newTorSettings: TorSettings) { + torSettings = newTorSettings + } + + fun updateAcceptedTerms(newAcceptedTerms: Boolean) { + acceptedTerms = newAcceptedTerms + if (newAcceptedTerms) { + termsAcceptanceIsRequiredError = false + } + + errorManager.clearErrors() + } + + fun checkCanSignup(): Boolean { + if (!acceptedTerms) { + termsAcceptanceIsRequiredError = true + return false + } + + if (displayName.text.isBlank()) { + errorManager.error(R.string.name_is_required) + return false + } + + return true + } + + fun signup() { + if (checkCanSignup()) { + accountStateViewModel.newKey(torSettings, displayName.text) + } + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 82a56524ee..0ab41e9cf7 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -836,6 +836,7 @@ Wallet %1$s Error opening signer app The signer app could not be found. Check if the app hasn\'t been uninstalled + Sign request rejected Signer Application Rejected Make sure the signer application has authorized this transaction diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt index f744f3ff96..cea5d7867a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.core +import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.jackson.EventManualSerializer import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper @@ -58,6 +59,14 @@ open class Event( companion object { fun fromJson(json: String): Event = JsonMapper.fromJson(json) + fun fromJsonOrNull(json: String) = + try { + fromJson(json) + } catch (e: Exception) { + Log.e("Event", "Unable to parse event JSON: $json", e) + null + } + fun build( kind: Int, content: String = "", diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt index 004812514c..82d0264c29 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt @@ -35,9 +35,13 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Request import com.vitorpamplona.quartz.nip47WalletConnect.RequestDeserializer import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip47WalletConnect.ResponseDeserializer +import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission +import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.PermissionDeserializer +import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.PermissionSerializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorSerializer +import kotlin.jvm.java class JsonMapper { companion object Companion { @@ -60,7 +64,9 @@ class JsonMapper { .addSerializer(BunkerRequest::class.java, BunkerRequest.BunkerRequestSerializer()) .addDeserializer(BunkerRequest::class.java, BunkerRequest.BunkerRequestDeserializer()) .addSerializer(BunkerResponse::class.java, BunkerResponse.BunkerResponseSerializer()) - .addDeserializer(BunkerResponse::class.java, BunkerResponse.BunkerResponseDeserializer()), + .addDeserializer(BunkerResponse::class.java, BunkerResponse.BunkerResponseDeserializer()) + .addDeserializer(Permission::class.java, PermissionDeserializer()) + .addSerializer(Permission::class.java, PermissionSerializer()), ) fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt index 0dfe23c984..c5e3635d0e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt @@ -47,25 +47,25 @@ abstract class NostrSigner( ) abstract fun nip04Encrypt( - decryptedContent: String, + plaintext: String, toPublicKey: HexKey, onReady: (String) -> Unit, ) abstract fun nip04Decrypt( - encryptedContent: String, + ciphertext: String, fromPublicKey: HexKey, onReady: (String) -> Unit, ) abstract fun nip44Encrypt( - decryptedContent: String, + plaintext: String, toPublicKey: HexKey, onReady: (String) -> Unit, ) abstract fun nip44Decrypt( - encryptedContent: String, + ciphertext: String, fromPublicKey: HexKey, onReady: (String) -> Unit, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt index 9a840abffe..d441e9193b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt @@ -45,35 +45,35 @@ class NostrSignerInternal( } override fun nip04Encrypt( - decryptedContent: String, + plaintext: String, toPublicKey: HexKey, onReady: (String) -> Unit, ) { - signerSync.nip04Encrypt(decryptedContent, toPublicKey)?.let { onReady(it) } + signerSync.nip04Encrypt(plaintext, toPublicKey)?.let { onReady(it) } } override fun nip04Decrypt( - encryptedContent: String, + ciphertext: String, fromPublicKey: HexKey, onReady: (String) -> Unit, ) { - signerSync.nip04Decrypt(encryptedContent, fromPublicKey)?.let { onReady(it) } + signerSync.nip04Decrypt(ciphertext, fromPublicKey)?.let { onReady(it) } } override fun nip44Encrypt( - decryptedContent: String, + plaintext: String, toPublicKey: HexKey, onReady: (String) -> Unit, ) { - signerSync.nip44Encrypt(decryptedContent, toPublicKey)?.let { onReady(it) } + signerSync.nip44Encrypt(plaintext, toPublicKey)?.let { onReady(it) } } override fun nip44Decrypt( - encryptedContent: String, + ciphertext: String, fromPublicKey: HexKey, onReady: (String) -> Unit, ) { - signerSync.nip44Decrypt(encryptedContent, fromPublicKey)?.let { onReady(it) } + signerSync.nip44Decrypt(ciphertext, fromPublicKey)?.let { onReady(it) } } override fun decryptZapEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt deleted file mode 100644 index ff7c5e4c4f..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/ExternalSignerLauncher.kt +++ /dev/null @@ -1,438 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip55AndroidSigner - -import android.content.ContentResolver -import android.content.Intent -import android.util.Log -import android.util.LruCache -import androidx.core.net.toUri -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.DeserializationFeature -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.deser.std.StdDeserializer -import com.fasterxml.jackson.databind.module.SimpleModule -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent - -enum class SignerType { - SIGN_EVENT, - NIP04_ENCRYPT, - NIP04_DECRYPT, - NIP44_ENCRYPT, - NIP44_DECRYPT, - GET_PUBLIC_KEY, - DECRYPT_ZAP_EVENT, - DERIVE_KEY, -} - -class Permission( - val type: String, - val kind: Int? = null, -) { - fun toJson(): String = "{\"type\":\"${type}\",\"kind\":$kind}" -} - -class Result( - @JsonProperty("package") val `package`: String?, - @JsonProperty("signature") val signature: String?, - @JsonProperty("result") val result: String?, - @JsonProperty("id") val id: String?, -) { - companion object { - val mapper: ObjectMapper = - jacksonObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .registerModule( - SimpleModule().addDeserializer(Result::class.java, ResultDeserializer()), - ) - - private class ResultDeserializer : StdDeserializer(Result::class.java) { - override fun deserialize( - jp: JsonParser, - ctxt: DeserializationContext, - ): Result { - val jsonObject: JsonNode = jp.codec.readTree(jp) - return Result( - jsonObject.get("package").asText().intern(), - jsonObject.get("signature")?.asText()?.intern(), - jsonObject.get("result")?.asText()?.intern(), - jsonObject.get("id").asText().intern(), - ) - } - } - - fun fromJson(json: String): Result = mapper.readValue(json, Result::class.java) - - /** - * Parses the json with a string of events to an Array of Event objects. - */ - fun fromJsonArray(json: String): Array = mapper.readValue(json) - } -} - -class ExternalSignerLauncher( - private val currentUserPubKeyHex: String, - val signerPackageName: String, -) { - private val contentCache = LruCache Unit>(50) - - private var signerAppLauncher: ((Intent) -> Unit)? = null - private var contentResolver: (() -> ContentResolver)? = null - - /** Call this function when the launcher becomes available on activity, fragment or compose */ - fun registerLauncher( - launcher: ((Intent) -> Unit), - contentResolver: (() -> ContentResolver), - ) { - this.signerAppLauncher = launcher - this.contentResolver = contentResolver - } - - /** Call this function when the activity is destroyed or is about to be replaced. */ - fun clearLauncherIf(launcher: ((Intent) -> Unit)) { - if (signerAppLauncher == launcher) { - this.signerAppLauncher = null - this.contentResolver = null - } - } - - fun newResult(data: Intent) { - val results = data.getStringExtra("results") - if (results != null) { - val localResults: Array = Result.fromJsonArray(results) - localResults.forEach { - val signature = it.result ?: it.signature ?: "" - val packageName = it.`package`?.let { "-$it" } ?: "" - val id = it.id ?: "" - if (id.isNotBlank()) { - val result = if (packageName.isNotBlank()) "$signature$packageName" else signature - val contentCache = contentCache.get(id) - contentCache?.invoke(result) - } - } - } else { - val signature = data.getStringExtra("result") ?: data.getStringExtra("signature") ?: "" - val packageName = data.getStringExtra("package")?.let { "-$it" } ?: "" - val id = data.getStringExtra("id") ?: "" - if (id.isNotBlank()) { - val result = if (packageName.isNotBlank()) "$signature$packageName" else signature - val contentCache = contentCache.get(id) - contentCache?.invoke(result) - } - } - } - - fun openSignerApp( - data: String, - type: SignerType, - pubKey: HexKey, - id: String, - onReady: (String) -> Unit, - ) { - signerAppLauncher?.let { - openSignerApp( - data, - type, - it, - pubKey, - id, - onReady, - ) - } - } - - private fun defaultPermissions(): String { - val permissions = - listOf( - Permission( - "sign_event", - 22242, - ), - Permission( - "sign_event", - 31234, - ), - Permission( - "nip04_encrypt", - ), - Permission( - "nip04_decrypt", - ), - Permission( - "nip44_encrypt", - ), - Permission( - "nip44_decrypt", - ), - Permission( - "decrypt_zap_event", - ), - ) - val jsonArray = StringBuilder("[") - permissions.forEachIndexed { index, permission -> - jsonArray.append(permission.toJson()) - if (index < permissions.size - 1) { - jsonArray.append(",") - } - } - jsonArray.append("]") - - return jsonArray.toString() - } - - private fun openSignerApp( - data: String, - type: SignerType, - intentLauncher: (Intent) -> Unit, - pubKey: HexKey, - id: String, - onReady: (String) -> Unit, - ) { - val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$data".toUri()) - val signerType = - when (type) { - SignerType.SIGN_EVENT -> "sign_event" - SignerType.NIP04_ENCRYPT -> "nip04_encrypt" - SignerType.NIP04_DECRYPT -> "nip04_decrypt" - SignerType.NIP44_ENCRYPT -> "nip44_encrypt" - SignerType.NIP44_DECRYPT -> "nip44_decrypt" - SignerType.GET_PUBLIC_KEY -> "get_public_key" - SignerType.DECRYPT_ZAP_EVENT -> "decrypt_zap_event" - SignerType.DERIVE_KEY -> "derive_key" - } - intent.putExtra("type", signerType) - intent.putExtra("pubKey", pubKey) - intent.putExtra("id", id) - if (type !== SignerType.GET_PUBLIC_KEY) { - intent.putExtra("current_user", currentUserPubKeyHex) - } else { - intent.putExtra("permissions", defaultPermissions()) - } - if (signerPackageName.isNotBlank()) { - intent.`package` = signerPackageName - } - - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) - - contentCache.put(id, onReady) - - intentLauncher(intent) - } - - fun openSigner( - event: Event, - onReady: (String) -> Unit, - ) { - getDataFromResolver( - SignerType.SIGN_EVENT, - arrayOf(event.toJson(), event.pubKey), - ).fold( - onFailure = { }, - onSuccess = { - if (it == null) { - openSignerApp( - event.toJson(), - SignerType.SIGN_EVENT, - "", - event.id, - onReady, - ) - } else { - onReady(it) - } - }, - ) - } - - private fun getDataFromResolver( - signerType: SignerType, - data: Array, - ): kotlin.Result = getDataFromResolver(signerType, data, contentResolver) - - private fun getDataFromResolver( - signerType: SignerType, - data: Array, - contentResolver: (() -> ContentResolver)? = null, - ): kotlin.Result { - val localData = - if (signerType !== SignerType.GET_PUBLIC_KEY) { - arrayOf(*data, currentUserPubKeyHex) - } else { - data - } - - try { - contentResolver - ?.let { it() } - ?.query( - "content://$signerPackageName.$signerType".toUri(), - localData, - "1", - null, - null, - ).use { - if (it == null) { - return kotlin.Result.success(null) - } - if (it.moveToFirst()) { - if (it.getColumnIndex("rejected") > -1) { - Log.d("getDataFromResolver", "Permission denied") - return kotlin.Result.failure(Exception("Permission denied")) - } - var index = it.getColumnIndex("result") - if (index < 0) { - index = it.getColumnIndex("signature") - } - if (index < 0) { - Log.d("getDataFromResolver", "column 'signature' not found") - return kotlin.Result.success(null) - } - return kotlin.Result.success(it.getString(index)) - } - } - } catch (e: Exception) { - Log.e("ExternalSignerLauncher", "Failed to query the Signer app in the background", e) - return kotlin.Result.success(null) - } - - return kotlin.Result.success(null) - } - - fun hashCodeFields( - str1: String, - onReady: (String) -> Unit, - ): Int { - var result = str1.hashCode() - result = 31 * result + onReady.hashCode() - return result - } - - fun hashCodeFields( - str1: String, - str2: String, - onReady: (String) -> Unit, - ): Int { - var result = str1.hashCode() - result = 31 * result + str2.hashCode() - result = 31 * result + onReady.hashCode() - return result - } - - fun decrypt( - encryptedContent: String, - pubKey: HexKey, - signerType: SignerType = SignerType.NIP04_DECRYPT, - onReady: (String) -> Unit, - ) { - getDataFromResolver(signerType, arrayOf(encryptedContent, pubKey)).fold( - onFailure = { }, - onSuccess = { - if (it == null) { - openSignerApp( - encryptedContent, - signerType, - pubKey, - hashCodeFields(encryptedContent, pubKey, onReady).toString(), - onReady, - ) - } else { - onReady(it) - } - }, - ) - } - - fun encrypt( - decryptedContent: String, - pubKey: HexKey, - signerType: SignerType = SignerType.NIP04_ENCRYPT, - onReady: (String) -> Unit, - ) { - getDataFromResolver(signerType, arrayOf(decryptedContent, pubKey)).fold( - onFailure = { }, - onSuccess = { - if (it == null) { - openSignerApp( - decryptedContent, - signerType, - pubKey, - hashCodeFields(decryptedContent, pubKey, onReady).toString(), - onReady, - ) - } else { - onReady(it) - } - }, - ) - } - - fun decryptZapEvent( - event: LnZapRequestEvent, - onReady: (String) -> Unit, - ) { - getDataFromResolver(SignerType.DECRYPT_ZAP_EVENT, arrayOf(event.toJson(), event.pubKey)).fold( - onFailure = { }, - onSuccess = { - if (it == null) { - openSignerApp( - event.toJson(), - SignerType.DECRYPT_ZAP_EVENT, - event.pubKey, - event.id, - onReady, - ) - } else { - onReady(it) - } - }, - ) - } - - fun deriveKey( - nonce: HexKey, - signerType: SignerType = SignerType.DERIVE_KEY, - onReady: (String) -> Unit, - ) { - getDataFromResolver(signerType, arrayOf(nonce)).fold( - onFailure = { }, - onSuccess = { - if (it == null) { - openSignerApp( - nonce, - signerType, - "", - hashCodeFields(nonce, onReady).toString(), - onReady, - ) - } else { - onReady(it) - } - }, - ) - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/CommandType.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/CommandType.kt new file mode 100644 index 0000000000..6f738db186 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/CommandType.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api + +enum class CommandType( + val code: String, +) { + SIGN_EVENT("sign_event"), + NIP04_ENCRYPT("nip04_encrypt"), + NIP04_DECRYPT("nip04_decrypt"), + NIP44_ENCRYPT("nip44_encrypt"), + NIP44_DECRYPT("nip44_decrypt"), + GET_PUBLIC_KEY("get_public_key"), + DECRYPT_ZAP_EVENT("decrypt_zap_event"), + DERIVE_KEY("derive_key"), + ; + + companion object Companion { + fun parse(code: String): CommandType? = + when (code) { + SIGN_EVENT.code -> SIGN_EVENT + NIP04_ENCRYPT.code -> NIP04_ENCRYPT + NIP04_DECRYPT.code -> NIP04_DECRYPT + NIP44_ENCRYPT.code -> NIP44_ENCRYPT + NIP44_DECRYPT.code -> NIP44_DECRYPT + GET_PUBLIC_KEY.code -> GET_PUBLIC_KEY + DECRYPT_ZAP_EVENT.code -> DECRYPT_ZAP_EVENT + DERIVE_KEY.code -> DERIVE_KEY + else -> null + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt new file mode 100644 index 0000000000..b8018518bd --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent + +interface RequestAddressed + +sealed class SignerResult { + class Successful( + val result: T, + ) : SignerResult(), + RequestAddressed + + class Rejected : + SignerResult(), + RequestAddressed + + class ReceivedButCouldNotPerform( + message: String? = null, + ) : SignerResult(), + RequestAddressed + + class ReceivedButCouldNotParseEventFromResult( + val eventJson: String, + ) : SignerResult(), + RequestAddressed + + class ReceivedButCouldNotVerifyResultingEvent( + val invalidEvent: Event, + ) : SignerResult(), + RequestAddressed + + class ErrorExceptionCallingContentResolver( + val e: Exception? = null, + ) : SignerResult() + + class RequiresManualApproval : SignerResult() +} + +interface IResult + +data class PubKeyResult( + val pubkey: HexKey, + val packageName: String, +) : IResult + +data class SignResult( + val event: Event, +) : IResult + +data class EncryptionResult( + val ciphertext: String, +) : IResult + +data class DecryptionResult( + val plaintext: String, +) : IResult + +data class ZapEventDecryptionResult( + val privateEvent: LnZapPrivateEvent, +) : IResult + +data class DerivationResult( + val newPrivKey: HexKey, +) : IResult diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt new file mode 100644 index 0000000000..7bc2b83fa9 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries + +import android.content.ContentResolver +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.verify +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query +import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import kotlin.text.isNullOrBlank + +class DecryptZapQuery( + val loggedInUser: HexKey, + val packageName: String, + val contentResolver: ContentResolver, +) { + fun query(event: Event): SignerResult = + contentResolver.query( + "content://$packageName.${CommandType.DECRYPT_ZAP_EVENT}".toUri(), + arrayOf(event.toJson(), event.pubKey, loggedInUser), + ) { cursor -> + val decryptedEventAsJson = cursor.getStringByName("result") + if (!decryptedEventAsJson.isNullOrBlank()) { + if (decryptedEventAsJson.startsWith("{")) { + val event = Event.fromJsonOrNull(decryptedEventAsJson) as? LnZapPrivateEvent + if (event != null) { + if (event.verify()) { + SignerResult.Successful(ZapEventDecryptionResult(event)) + } else { + SignerResult.ReceivedButCouldNotVerifyResultingEvent(event) + } + } else { + SignerResult.ReceivedButCouldNotParseEventFromResult(decryptedEventAsJson) + } + } else { + SignerResult.ReceivedButCouldNotPerform(decryptedEventAsJson) + } + } else { + SignerResult.ReceivedButCouldNotPerform(decryptedEventAsJson) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DeriveKeyQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DeriveKeyQuery.kt new file mode 100644 index 0000000000..6d52621633 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DeriveKeyQuery.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries + +import android.content.ContentResolver +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query + +class DeriveKeyQuery( + val loggedInUser: HexKey, + val packageName: String, + val contentResolver: ContentResolver, +) { + val uri = "content://$packageName.${CommandType.DERIVE_KEY}".toUri() + + fun query(nonce: HexKey): SignerResult = + contentResolver.query( + uri, + arrayOf(nonce, loggedInUser), + ) { cursor -> + val newPrivateKey = cursor.getStringByName("result") + if (!newPrivateKey.isNullOrBlank()) { + SignerResult.Successful(DerivationResult(newPrivateKey)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/LoginQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/LoginQuery.kt new file mode 100644 index 0000000000..9ab83d9687 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/LoginQuery.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries + +import android.content.ContentResolver +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query + +class LoginQuery( + val packageName: String, + val contentResolver: ContentResolver, +) { + companion object { + val LOGIN = arrayOf("login") + } + + val uri = "content://$packageName.${CommandType.GET_PUBLIC_KEY}".toUri() + + fun query(): SignerResult = + contentResolver.query( + uri, + LOGIN, + ) { cursor -> + val pubkeyHex = cursor.getStringByName("result") + if (!pubkeyHex.isNullOrBlank()) { + SignerResult.Successful(PubKeyResult(pubkeyHex, packageName)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04DecryptQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04DecryptQuery.kt new file mode 100644 index 0000000000..35bc8cb1da --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04DecryptQuery.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries + +import android.content.ContentResolver +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query + +class Nip04DecryptQuery( + val loggedInUser: HexKey, + val packageName: String, + val contentResolver: ContentResolver, +) { + val uri = "content://$packageName.${CommandType.NIP04_DECRYPT}".toUri() + + fun query( + ciphertext: String, + fromPubKey: HexKey, + ): SignerResult = + contentResolver.query( + uri, + arrayOf(ciphertext, fromPubKey, loggedInUser), + ) { cursor -> + val plaintext = cursor.getStringByName("result") + if (!plaintext.isNullOrBlank()) { + SignerResult.Successful(DecryptionResult(plaintext)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04EncryptQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04EncryptQuery.kt new file mode 100644 index 0000000000..96273ae6ee --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04EncryptQuery.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries + +import android.content.ContentResolver +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query + +class Nip04EncryptQuery( + val loggedInUser: HexKey, + val packageName: String, + val contentResolver: ContentResolver, +) { + val uri = "content://$packageName.${CommandType.NIP04_ENCRYPT}".toUri() + + fun query( + plaintext: String, + toPubKey: HexKey, + ): SignerResult = + contentResolver.query( + uri, + arrayOf(plaintext, toPubKey, loggedInUser), + ) { cursor -> + val ciphertext = cursor.getStringByName("result") + if (!ciphertext.isNullOrBlank()) { + SignerResult.Successful(EncryptionResult(ciphertext)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44DecryptQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44DecryptQuery.kt new file mode 100644 index 0000000000..844143b292 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44DecryptQuery.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries + +import android.content.ContentResolver +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query + +class Nip44DecryptQuery( + val loggedInUser: HexKey, + val packageName: String, + val contentResolver: ContentResolver, +) { + val uri = "content://$packageName.${CommandType.NIP44_DECRYPT}".toUri() + + fun query( + ciphertext: String, + fromPubKey: HexKey, + ): SignerResult = + contentResolver.query( + uri, + arrayOf(ciphertext, fromPubKey, loggedInUser), + ) { cursor -> + val plaintext = cursor.getStringByName("result") + if (!plaintext.isNullOrBlank()) { + SignerResult.Successful(DecryptionResult(plaintext)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44EncryptQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44EncryptQuery.kt new file mode 100644 index 0000000000..37deecc04b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44EncryptQuery.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries + +import android.content.ContentResolver +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query + +class Nip44EncryptQuery( + val loggedInUser: HexKey, + val packageName: String, + val contentResolver: ContentResolver, +) { + val uri = "content://$packageName.${CommandType.NIP44_ENCRYPT}".toUri() + + fun query( + plaintext: String, + toPubKey: HexKey, + ): SignerResult = + contentResolver.query( + uri, + arrayOf(plaintext, toPubKey, loggedInUser), + ) { cursor -> + val ciphertext = cursor.getStringByName("result") + if (!ciphertext.isNullOrBlank()) { + SignerResult.Successful(EncryptionResult(ciphertext)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/SignQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/SignQuery.kt new file mode 100644 index 0000000000..b665c667f0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/SignQuery.kt @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries + +import android.content.ContentResolver +import androidx.core.net.toUri +import com.vitorpamplona.quartz.EventFactory +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.verify +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query + +class SignQuery( + val loggedInUser: HexKey, + val packageName: String, + val contentResolver: ContentResolver, +) { + val uri = "content://$packageName.${CommandType.SIGN_EVENT}".toUri() + + fun query(unsignedEvent: Event): SignerResult = + contentResolver.query( + uri, + arrayOf(unsignedEvent.toJson(), unsignedEvent.pubKey, loggedInUser), + ) { cursor -> + val eventJson = cursor.getStringByName("event") + if (!eventJson.isNullOrBlank()) { + if (eventJson.startsWith("{")) { + val event = Event.fromJsonOrNull(eventJson) + if (event != null) { + if (event.verify()) { + SignerResult.Successful(SignResult(event)) + } else { + SignerResult.ReceivedButCouldNotVerifyResultingEvent(event) + } + } else { + SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson) + } + } else { + SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson) + } + } else { + val signature = cursor.getStringByName("result") + if (!signature.isNullOrBlank()) { + val event = + EventFactory.create( + id = unsignedEvent.id, + pubKey = unsignedEvent.pubKey, + createdAt = unsignedEvent.createdAt, + kind = unsignedEvent.kind, + tags = unsignedEvent.tags, + content = unsignedEvent.content, + sig = signature, + ) + if (event.verify()) { + SignerResult.Successful(SignResult(event)) + } else { + SignerResult.ReceivedButCouldNotVerifyResultingEvent(event) + } + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/ContentResolverExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/ContentResolverExt.kt new file mode 100644 index 0000000000..9dcb25e4e5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/ContentResolverExt.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils + +import android.content.ContentResolver +import android.database.Cursor +import android.net.Uri +import android.util.Log +import com.vitorpamplona.quartz.nip55AndroidSigner.api.IResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult + +fun ContentResolver.query( + uri: Uri, + projection: Array, + map: (cursor: Cursor) -> SignerResult, +): SignerResult = + try { + query( + uri, + projection, + null, + null, + null, + ).use { + if (it != null && it.moveToFirst()) { + if (it.getColumnIndex("rejected") > -1) { + SignerResult.Rejected() + } else { + map(it) + } + } else { + SignerResult.RequiresManualApproval() + } + } + } catch (e: Exception) { + Log.e("ExternalSignerLauncher", "Failed to query the Signer app in the background", e) + SignerResult.ErrorExceptionCallingContentResolver(e) + } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/CursorExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/CursorExt.kt new file mode 100644 index 0000000000..ca81cc000f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/CursorExt.kt @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils + +import android.database.Cursor + +fun Cursor.getStringByName(name: String): String? { + val index = getColumnIndex(name) + return if (index >= 0) { + val result = getString(index) + result.ifBlank { null } + } else { + null + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt new file mode 100644 index 0000000000..647f42cc23 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground + +import android.content.Intent +import androidx.collection.LruCache +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor +import com.vitorpamplona.quartz.utils.RandomInstance + +class IntentRequestDatabase { + private val awaitingRequests = LruCache(2000) + private var appLauncher: ((Intent) -> Unit)? = null + + /** Call this function when the launcher becomes available on activity, fragment or compose */ + fun registerForegroundLauncher(launcher: ((Intent) -> Unit)) { + this.appLauncher = launcher + } + + /** Call this function when the activity is destroyed or is about to be replaced. */ + fun unregisterForegroundLauncher(launcher: ((Intent) -> Unit)) { + if (this.appLauncher == launcher) { + this.appLauncher = null + } + } + + fun newResponse(data: Intent) { + val callId = data.getStringExtra("id") + if (callId != null) { + awaitingRequests[callId]?.process(data) + awaitingRequests.remove(callId) + } + } + + fun launch( + requestIntent: Intent, + responseProcessor: NewResultProcessor, + ) { + appLauncher?.let { + val callId = RandomInstance.randomChars(32) + awaitingRequests.put(callId, responseProcessor) + + requestIntent.putExtra("id", callId) + requestIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) + it.invoke(requestIntent) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt new file mode 100644 index 0000000000..8eda0d7f24 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground + +import android.content.Intent + +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +interface NewResultProcessor { + fun process(intent: Intent) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DecryptZapRequest.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DecryptZapRequest.kt new file mode 100644 index 0000000000..80ee7602be --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DecryptZapRequest.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests + +import android.content.Intent +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent + +class DecryptZapRequest { + companion object { + fun assemble( + event: LnZapRequestEvent, + loggedInUser: HexKey, + packageName: String, + ): Intent { + val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:${event.toJson()}".toUri()) + intent.`package` = packageName + intent.putExtra("type", CommandType.NIP44_DECRYPT.code) + intent.putExtra("current_user", loggedInUser) + return intent + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DeriveKeyRequest.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DeriveKeyRequest.kt new file mode 100644 index 0000000000..dac0401635 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/DeriveKeyRequest.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests + +import android.content.Intent +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType + +class DeriveKeyRequest { + companion object { + fun assemble( + nonce: HexKey, + loggedInUser: HexKey, + packageName: String, + ): Intent { + val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$nonce".toUri()) + intent.`package` = packageName + intent.putExtra("type", CommandType.DERIVE_KEY.code) + intent.putExtra("current_user", loggedInUser) + return intent + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/LoginRequest.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/LoginRequest.kt new file mode 100644 index 0000000000..39a02fa9c1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/LoginRequest.kt @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests + +import android.content.Intent +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType +import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission + +class LoginRequest { + companion object { + val DefaultPermissions = + listOf( + Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND), + Permission(CommandType.NIP04_ENCRYPT), + Permission(CommandType.NIP04_DECRYPT), + Permission(CommandType.NIP44_DECRYPT), + Permission(CommandType.NIP44_DECRYPT), + Permission(CommandType.DECRYPT_ZAP_EVENT), + ) + + fun assemble(permissions: List = DefaultPermissions): Intent { + val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:".toUri()) + intent.putExtra("type", CommandType.GET_PUBLIC_KEY.code) + intent.putExtra("permissions", JsonMapper.mapper.writeValueAsString(permissions)) + return intent + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip04DecryptRequest.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip04DecryptRequest.kt new file mode 100644 index 0000000000..a2e02e27f8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip04DecryptRequest.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests + +import android.content.Intent +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType + +class Nip04DecryptRequest { + companion object { + fun assemble( + ciphertext: String, + fromPubKey: HexKey, + loggedInUser: HexKey, + packageName: String, + ): Intent { + val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$ciphertext".toUri()) + intent.`package` = packageName + intent.putExtra("type", CommandType.NIP04_DECRYPT.code) + intent.putExtra("pubKey", fromPubKey) + intent.putExtra("current_user", loggedInUser) + return intent + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip04EncryptRequest.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip04EncryptRequest.kt new file mode 100644 index 0000000000..473cbf5f4b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip04EncryptRequest.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests + +import android.content.Intent +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType + +class Nip04EncryptRequest { + companion object { + fun assemble( + plaintext: String, + toPubKey: HexKey, + loggedInUser: HexKey, + packageName: String, + ): Intent { + val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$plaintext".toUri()) + intent.`package` = packageName + intent.putExtra("type", CommandType.NIP04_ENCRYPT.code) + intent.putExtra("pubKey", toPubKey) + intent.putExtra("current_user", loggedInUser) + return intent + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip44DecryptRequest.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip44DecryptRequest.kt new file mode 100644 index 0000000000..0cd72d53a1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip44DecryptRequest.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests + +import android.content.Intent +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType + +class Nip44DecryptRequest { + companion object { + fun assemble( + ciphertext: String, + fromPubKey: HexKey, + loggedInUser: HexKey, + packageName: String, + ): Intent { + val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$ciphertext".toUri()) + intent.`package` = packageName + intent.putExtra("type", CommandType.NIP44_DECRYPT.code) + intent.putExtra("pubKey", fromPubKey) + intent.putExtra("current_user", loggedInUser) + return intent + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip44EncryptRequest.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip44EncryptRequest.kt new file mode 100644 index 0000000000..4e19ffcb2f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/Nip44EncryptRequest.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests + +import android.content.Intent +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType + +class Nip44EncryptRequest { + companion object { + fun assemble( + plaintext: String, + toPubKey: HexKey, + loggedInUser: HexKey, + packageName: String, + ): Intent { + val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$plaintext".toUri()) + intent.`package` = packageName + intent.putExtra("type", CommandType.NIP44_ENCRYPT.code) + intent.putExtra("pubKey", toPubKey) + intent.putExtra("current_user", loggedInUser) + return intent + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/SignRequest.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/SignRequest.kt new file mode 100644 index 0000000000..55136388dc --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/requests/SignRequest.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests + +import android.content.Intent +import androidx.core.net.toUri +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType + +class SignRequest { + companion object { + fun assemble( + event: Event, + loggedInUser: HexKey, + packageName: String, + ): Intent { + val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:${event.toJson()}".toUri()) + intent.`package` = packageName + intent.putExtra("type", CommandType.SIGN_EVENT.code) + intent.putExtra("current_user", loggedInUser) + return intent + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt new file mode 100644 index 0000000000..4c7fa632b4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses + +import android.content.Intent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult +import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent + +class DecryptZapResponse { + companion object { + fun assemble(event: LnZapPrivateEvent): Intent { + val intent = Intent() + intent.putExtra("result", event.toJson()) + return intent + } + + fun parse(intent: Intent): SignerResult { + val eventJson = intent.getStringExtra("result") + return if (!eventJson.isNullOrBlank()) { + if (eventJson.startsWith("{")) { + val event = Event.fromJsonOrNull(eventJson) as? LnZapPrivateEvent + if (event != null) { + SignerResult.Successful(ZapEventDecryptionResult(event)) + } else { + SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson) + } + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt new file mode 100644 index 0000000000..8288371398 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult + +class DeriveKeyResponse { + companion object { + fun parse(intent: Intent): SignerResult { + val newPrivateKey = intent.getStringExtra("result") + return if (newPrivateKey != null) { + SignerResult.Successful(DerivationResult(newPrivateKey)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt new file mode 100644 index 0000000000..27c3901e8d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult + +class LoginResponse { + companion object { + fun parse(intent: Intent): SignerResult { + val pubkey = intent.getStringExtra("result") + val packageName = intent.getStringExtra("package") + + return if (pubkey != null && packageName != null) { + SignerResult.Successful( + PubKeyResult( + pubkey = pubkey, + packageName = packageName, + ), + ) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt new file mode 100644 index 0000000000..accae93b25 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult + +class Nip04DecryptResponse { + companion object { + fun parse(intent: Intent): SignerResult { + val ciphertext = intent.getStringExtra("result") + return if (ciphertext != null) { + SignerResult.Successful(DecryptionResult(ciphertext)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt new file mode 100644 index 0000000000..dc7e167d6f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult + +class Nip04EncryptResponse { + companion object { + fun parse(intent: Intent): SignerResult { + val ciphertext = intent.getStringExtra("result") + return if (ciphertext != null) { + SignerResult.Successful(EncryptionResult(ciphertext)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt new file mode 100644 index 0000000000..9d63a6899d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult + +class Nip44DecryptResponse { + companion object { + fun parse(intent: Intent): SignerResult { + val ciphertext = intent.getStringExtra("result") + return if (ciphertext != null) { + SignerResult.Successful(DecryptionResult(ciphertext)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt new file mode 100644 index 0000000000..180a81f6e1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult + +class Nip44EncryptResponse { + companion object { + fun parse(intent: Intent): SignerResult { + val ciphertext = intent.getStringExtra("result") + return if (ciphertext != null) { + SignerResult.Successful(EncryptionResult(ciphertext)) + } else { + SignerResult.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt new file mode 100644 index 0000000000..88a13b8c09 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses + +import android.content.Intent +import com.vitorpamplona.quartz.EventFactory +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.verify +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult + +class SignResponse { + companion object { + fun parse( + intent: Intent, + unsignedEvent: Event, + ): SignerResult { + val eventJson = intent.getStringExtra("event") + return if (eventJson != null) { + if (eventJson.startsWith("{")) { + val event = Event.fromJsonOrNull(eventJson) + if (event != null) { + if (event.verify()) { + SignerResult.Successful(SignResult(event)) + } else { + SignerResult.ReceivedButCouldNotVerifyResultingEvent(event) + } + } else { + SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson) + } + } else { + SignerResult.ReceivedButCouldNotPerform(eventJson) + } + } else { + val signature = intent.getStringExtra("result") + if (signature != null && signature.length == 128) { + val event = + EventFactory.create( + id = unsignedEvent.id, + pubKey = unsignedEvent.pubKey, + createdAt = unsignedEvent.createdAt, + kind = unsignedEvent.kind, + tags = unsignedEvent.tags, + content = unsignedEvent.content, + sig = signature, + ) + if (event.verify()) { + SignerResult.Successful(SignResult(event)) + } else { + SignerResult.ReceivedButCouldNotVerifyResultingEvent(event) + } + } else { + SignerResult.ReceivedButCouldNotPerform(signature) + } + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt new file mode 100644 index 0000000000..ff141d1065 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DecryptZapResponse +import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent + +class DecryptZapResultProcessor( + val onReady: (LnZapPrivateEvent) -> Unit, +) : NewResultProcessor { + override fun process(intent: Intent) { + val foregroundResult = DecryptZapResponse.parse(intent) + + if (foregroundResult is SignerResult.Successful) { + onReady(foregroundResult.result.privateEvent) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt new file mode 100644 index 0000000000..ed17c49f30 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DeriveKeyResponse + +class DeriveKeyResultProcessor( + val onReady: (String) -> Unit, +) : NewResultProcessor { + override fun process(intent: Intent) { + val foregroundResult = DeriveKeyResponse.parse(intent) + + if (foregroundResult is SignerResult.Successful) { + onReady(foregroundResult.result.newPrivKey) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt new file mode 100644 index 0000000000..5d4eda273f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors + +import android.content.Intent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.LoginResponse + +class LoginResultProcessor( + val onReady: (HexKey, String) -> Unit, +) : NewResultProcessor { + override fun process(intent: Intent) { + val foregroundResult = LoginResponse.parse(intent) + + if (foregroundResult is SignerResult.Successful) { + onReady(foregroundResult.result.pubkey, foregroundResult.result.packageName) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt new file mode 100644 index 0000000000..a2c3135233 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip04DecryptResponse + +class Nip04DecryptResultProcessor( + val onReady: (String) -> Unit, +) : NewResultProcessor { + override fun process(intent: Intent) { + val foregroundResult = Nip04DecryptResponse.parse(intent) + + if (foregroundResult is SignerResult.Successful) { + onReady(foregroundResult.result.plaintext) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt new file mode 100644 index 0000000000..de86a1c072 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip04EncryptResponse + +class Nip04EncryptResultProcessor( + val onReady: (String) -> Unit, +) : NewResultProcessor { + override fun process(intent: Intent) { + val foregroundResult = Nip04EncryptResponse.parse(intent) + + if (foregroundResult is SignerResult.Successful) { + onReady(foregroundResult.result.ciphertext) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt new file mode 100644 index 0000000000..45afd04ce3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44DecryptResponse + +class Nip44DecryptResultProcessor( + val onReady: (String) -> Unit, +) : NewResultProcessor { + override fun process(intent: Intent) { + val foregroundResult = Nip44DecryptResponse.parse(intent) + + if (foregroundResult is SignerResult.Successful) { + onReady(foregroundResult.result.plaintext) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt new file mode 100644 index 0000000000..9b6d9e6783 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44EncryptResponse + +class Nip44EncryptResultProcessor( + val onReady: (String) -> Unit, +) : NewResultProcessor { + override fun process(intent: Intent) { + val foregroundResult = Nip44EncryptResponse.parse(intent) + if (foregroundResult is SignerResult.Successful) { + onReady(foregroundResult.result.ciphertext) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt new file mode 100644 index 0000000000..7fd670a875 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors + +import android.content.Intent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.SignResponse + +class SignResultProcessor( + val unsignedEvent: Event, + val onReady: (Event) -> Unit, +) : NewResultProcessor { + override fun process(intent: Intent) { + val foregroundResult = SignResponse.parse(intent, unsignedEvent) + + if (foregroundResult is SignerResult.Successful) { + onReady(foregroundResult.result.event) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/Permission.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/Permission.kt new file mode 100644 index 0000000000..9c1511b532 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/Permission.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.permission + +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType + +class Permission( + val type: CommandType?, + val kind: Int? = null, +) { + fun toJson(): String = JsonMapper.mapper.writeValueAsString(this) + + companion object { + fun fromJson(json: String): Permission = JsonMapper.mapper.readValue(json, Permission::class.java) + + fun fromJsonArray(json: String): Array = JsonMapper.mapper.readValue(json, Array::class.java) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/PermissionDeserializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/PermissionDeserializer.kt new file mode 100644 index 0000000000..ebea3de4b1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/PermissionDeserializer.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.permission + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType + +class PermissionDeserializer : StdDeserializer(Permission::class.java) { + override fun deserialize( + jp: JsonParser, + ctxt: DeserializationContext, + ): Permission { + val jsonObject: JsonNode = jp.codec.readTree(jp) + return Permission( + type = CommandType.parse(jsonObject.get("type").asText()), + kind = jsonObject.get("kind")?.asInt(), + ) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/PermissionSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/PermissionSerializer.kt new file mode 100644 index 0000000000..3d53505314 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/permission/PermissionSerializer.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.permission + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer + +class PermissionSerializer : StdSerializer(Permission::class.java) { + override fun serialize( + permission: Permission, + gen: JsonGenerator, + provider: SerializerProvider, + ) { + gen.writeStartObject() + permission.type.let { gen.writeStringField("type", it?.code) } + if (permission.kind != null) { + gen.writeNumberField("kind", permission.kind) + } else { + gen.writeNullField("kind") + } + gen.writeEndObject() + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt new file mode 100644 index 0000000000..b5e3fb0926 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.client + +import android.content.Intent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.LoginRequest +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.LoginResponse +import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission + +object ExternalSignerLogin { + fun createIntent(permissions: List = LoginRequest.DefaultPermissions): Intent { + val intent = LoginRequest.assemble(permissions) + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) + return intent + } + + fun parseResult(data: Intent): SignerResult = LoginResponse.parse(data) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IActivityLauncher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IActivityLauncher.kt new file mode 100644 index 0000000000..af3f02ade2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IActivityLauncher.kt @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.client + +import android.content.Intent + +interface IActivityLauncher { + fun registerForegroundLauncher(launcher: ((Intent) -> Unit)) + + fun unregisterForegroundLauncher(launcher: ((Intent) -> Unit)) + + fun newResponse(data: Intent) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IsExternalSignerInstalled.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IsExternalSignerInstalled.kt new file mode 100644 index 0000000000..92d7f5e48c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IsExternalSignerInstalled.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.client + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri + +@SuppressLint("QueryPermissionsNeeded") +fun isExternalSignerInstalled(context: Context): Boolean = + context.packageManager + .queryIntentActivities( + Intent().apply { + action = Intent.ACTION_VIEW + data = "nostrsigner:".toUri() + }, + 0, + ).isNotEmpty() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt similarity index 53% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt index 2b7d9ad680..f1ffb4fbd4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt @@ -18,23 +18,42 @@ * 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.nip55AndroidSigner +package com.vitorpamplona.quartz.nip55AndroidSigner.client -import android.util.Log -import com.vitorpamplona.quartz.EventFactory +import android.content.ContentResolver +import android.content.Intent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers.BackgroundRequestHandler +import com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers.ForegroundRequestHandler import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent class NostrSignerExternal( pubKey: HexKey, - val launcher: ExternalSignerLauncher, -) : NostrSigner(pubKey) { + packageName: String, + contentResolver: ContentResolver, +) : NostrSigner(pubKey), + IActivityLauncher { override fun isWriteable(): Boolean = true + val backgroundQuery = BackgroundRequestHandler(pubKey, packageName, contentResolver) + val foregroundQuery = ForegroundRequestHandler(pubKey, packageName) + + override fun registerForegroundLauncher(launcher: ((Intent) -> Unit)) { + this.foregroundQuery.launcher.registerForegroundLauncher(launcher) + } + + override fun unregisterForegroundLauncher(launcher: ((Intent) -> Unit)) { + this.foregroundQuery.launcher.unregisterForegroundLauncher(launcher) + } + + override fun newResponse(data: Intent) { + this.foregroundQuery.launcher.newResponse(data) + } + override fun sign( createdAt: Long, kind: Int, @@ -42,7 +61,7 @@ class NostrSignerExternal( content: String, onReady: (T) -> Unit, ) { - val event = + val unsignedEvent = Event( id = EventHasher.hashId(pubKey, createdAt, kind, tags, content), pubKey = pubKey, @@ -53,109 +72,70 @@ class NostrSignerExternal( sig = "", ) - launcher.openSigner(event) { signature -> - if (signature.startsWith("{")) { - val localEvent = Event.fromJson(signature) - ( - EventFactory.create( - localEvent.id, - localEvent.pubKey, - localEvent.createdAt, - localEvent.kind, - localEvent.tags, - localEvent.content, - localEvent.sig, - ) as? T? - )?.let { onReady(it) } - } else { - ( - EventFactory.create( - event.id, - event.pubKey, - event.createdAt, - event.kind, - event.tags, - event.content, - signature.split("-")[0], - ) as? T? - )?.let { onReady(it) } - } + val newOnReady: (Event) -> Unit = { result -> + (result as? T)?.let(onReady) + } + + if (!backgroundQuery.sign(unsignedEvent, newOnReady)) { + foregroundQuery.sign(unsignedEvent, newOnReady) } } override fun nip04Encrypt( - decryptedContent: String, + plaintext: String, toPublicKey: HexKey, onReady: (String) -> Unit, ) { - launcher.encrypt( - decryptedContent, - toPublicKey, - SignerType.NIP04_ENCRYPT, - onReady, - ) + if (!backgroundQuery.nip04Encrypt(plaintext, toPublicKey, onReady)) { + foregroundQuery.nip04Encrypt(plaintext, toPublicKey, onReady) + } } override fun nip04Decrypt( - encryptedContent: String, + ciphertext: String, fromPublicKey: HexKey, onReady: (String) -> Unit, ) { - launcher.decrypt( - encryptedContent, - fromPublicKey, - SignerType.NIP04_DECRYPT, - onReady, - ) + if (!backgroundQuery.nip04Decrypt(ciphertext, fromPublicKey, onReady)) { + foregroundQuery.nip04Decrypt(ciphertext, fromPublicKey, onReady) + } } override fun nip44Encrypt( - decryptedContent: String, + plaintext: String, toPublicKey: HexKey, onReady: (String) -> Unit, ) { - launcher.encrypt( - decryptedContent, - toPublicKey, - SignerType.NIP44_ENCRYPT, - onReady, - ) + if (!backgroundQuery.nip44Encrypt(plaintext, toPublicKey, onReady)) { + foregroundQuery.nip44Encrypt(plaintext, toPublicKey, onReady) + } } override fun nip44Decrypt( - encryptedContent: String, + ciphertext: String, fromPublicKey: HexKey, onReady: (String) -> Unit, ) { - launcher.decrypt( - encryptedContent, - fromPublicKey, - SignerType.NIP44_DECRYPT, - onReady, - ) + if (!backgroundQuery.nip44Decrypt(ciphertext, fromPublicKey, onReady)) { + foregroundQuery.nip44Decrypt(ciphertext, fromPublicKey, onReady) + } } override fun deriveKey( nonce: HexKey, onReady: (HexKey) -> Unit, ) { - launcher.deriveKey( - nonce, - SignerType.DERIVE_KEY, - onReady, - ) + if (!backgroundQuery.deriveKey(nonce, onReady)) { + foregroundQuery.deriveKey(nonce, onReady) + } } override fun decryptZapEvent( event: LnZapRequestEvent, onReady: (LnZapPrivateEvent) -> Unit, ) { - launcher.decryptZapEvent(event) { jsonEvent -> - try { - (Event.fromJson(jsonEvent) as? LnZapPrivateEvent)?.let { onReady(it) } - } catch (e: Exception) { - Log.e("NostrExternalSigner", "Unable to parse returned decrypted Zap: $jsonEvent") - } + if (!backgroundQuery.decryptZapEvent(event, onReady)) { + foregroundQuery.decryptZapEvent(event, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt new file mode 100644 index 0000000000..9edea8f543 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers + +import android.content.ContentResolver +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.RequestAddressed +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.DecryptZapQuery +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.DeriveKeyQuery +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.LoginQuery +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip04DecryptQuery +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip04EncryptQuery +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44DecryptQuery +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44EncryptQuery +import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.SignQuery +import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent + +class BackgroundRequestHandler( + loggedInUser: HexKey, + packageName: String, + contentResolver: ContentResolver, +) { + val login = LoginQuery(packageName, contentResolver) + val sign = SignQuery(loggedInUser, packageName, contentResolver) + val nip04Encrypt = Nip04EncryptQuery(loggedInUser, packageName, contentResolver) + val nip04Decrypt = Nip04DecryptQuery(loggedInUser, packageName, contentResolver) + val nip44Encrypt = Nip44EncryptQuery(loggedInUser, packageName, contentResolver) + val nip44Decrypt = Nip44DecryptQuery(loggedInUser, packageName, contentResolver) + val decryptZap = DecryptZapQuery(loggedInUser, packageName, contentResolver) + val deriveKey = DeriveKeyQuery(loggedInUser, packageName, contentResolver) + + fun login(onReady: (HexKey) -> Unit): Boolean { + val backgroundResult = login.query() + + if (backgroundResult is SignerResult.Successful) { + onReady(backgroundResult.result.pubkey) + } + + return backgroundResult is RequestAddressed + } + + fun sign( + unsignedEvent: Event, + onReady: (Event) -> Unit, + ): Boolean { + val backgroundResult = sign.query(unsignedEvent) + + if (backgroundResult is SignerResult.Successful) { + onReady(backgroundResult.result.event) + } + + return backgroundResult is RequestAddressed + } + + fun nip04Encrypt( + plaintext: String, + toPubKey: HexKey, + onReady: (String) -> Unit, + ): Boolean { + val backgroundResult = nip04Encrypt.query(plaintext, toPubKey) + + if (backgroundResult is SignerResult.Successful) { + onReady(backgroundResult.result.ciphertext) + } + + return backgroundResult is RequestAddressed + } + + fun nip04Decrypt( + ciphertext: String, + fromPubKey: HexKey, + onReady: (String) -> Unit, + ): Boolean { + val backgroundResult = nip04Decrypt.query(ciphertext, fromPubKey) + + if (backgroundResult is SignerResult.Successful) { + onReady(backgroundResult.result.plaintext) + } + + return backgroundResult is RequestAddressed + } + + fun nip44Encrypt( + plaintext: String, + toPubKey: HexKey, + onReady: (String) -> Unit, + ): Boolean { + val backgroundResult = nip44Encrypt.query(plaintext, toPubKey) + + if (backgroundResult is SignerResult.Successful) { + onReady(backgroundResult.result.ciphertext) + } + + return backgroundResult is RequestAddressed + } + + fun nip44Decrypt( + ciphertext: String, + fromPubKey: HexKey, + onReady: (String) -> Unit, + ): Boolean { + val backgroundResult = nip44Decrypt.query(ciphertext, fromPubKey) + + if (backgroundResult is SignerResult.Successful) { + onReady(backgroundResult.result.plaintext) + } + + return backgroundResult is RequestAddressed + } + + fun decryptZapEvent( + event: LnZapRequestEvent, + onReady: (LnZapPrivateEvent) -> Unit, + ): Boolean { + val backgroundResult = decryptZap.query(event) + + if (backgroundResult is SignerResult.Successful) { + onReady(backgroundResult.result.privateEvent) + } + + return backgroundResult is RequestAddressed + } + + fun deriveKey( + nonce: HexKey, + onReady: (HexKey) -> Unit, + ): Boolean { + val backgroundResult = deriveKey.query(nonce) + + if (backgroundResult is SignerResult.Successful) { + onReady(backgroundResult.result.newPrivKey) + } + + return backgroundResult is RequestAddressed + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt new file mode 100644 index 0000000000..27b1584d28 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.IntentRequestDatabase +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.DecryptZapRequest +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.DeriveKeyRequest +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip04DecryptRequest +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip04EncryptRequest +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip44DecryptRequest +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip44EncryptRequest +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.SignRequest +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.DecryptZapResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.DeriveKeyResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip04DecryptResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip04EncryptResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip44DecryptResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip44EncryptResultProcessor +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.SignResultProcessor +import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent + +class ForegroundRequestHandler( + val loggedInUser: HexKey, + val packageName: String, +) { + val launcher = IntentRequestDatabase() + + fun sign( + unsignedEvent: Event, + onReady: (Event) -> Unit, + ) = launcher.launch( + SignRequest.assemble(unsignedEvent, loggedInUser, packageName), + SignResultProcessor(unsignedEvent, onReady), + ) + + fun nip04Encrypt( + plaintext: String, + toPubKey: HexKey, + onReady: (String) -> Unit, + ) = launcher.launch( + Nip04EncryptRequest.assemble(plaintext, toPubKey, loggedInUser, packageName), + Nip04EncryptResultProcessor(onReady), + ) + + fun nip04Decrypt( + ciphertext: String, + fromPubKey: HexKey, + onReady: (String) -> Unit, + ) = launcher.launch( + Nip04DecryptRequest.assemble(ciphertext, fromPubKey, loggedInUser, packageName), + Nip04DecryptResultProcessor(onReady), + ) + + fun nip44Encrypt( + plaintext: String, + toPubKey: HexKey, + onReady: (String) -> Unit, + ) = launcher.launch( + Nip44EncryptRequest.assemble(plaintext, toPubKey, loggedInUser, packageName), + Nip44EncryptResultProcessor(onReady), + ) + + fun nip44Decrypt( + ciphertext: String, + fromPubKey: HexKey, + onReady: (String) -> Unit, + ) = launcher.launch( + Nip44DecryptRequest.assemble(ciphertext, fromPubKey, loggedInUser, packageName), + Nip44DecryptResultProcessor(onReady), + ) + + fun decryptZapEvent( + event: LnZapRequestEvent, + onReady: (LnZapPrivateEvent) -> Unit, + ) = launcher.launch( + DecryptZapRequest.assemble(event, loggedInUser, packageName), + DecryptZapResultProcessor(onReady), + ) + + fun deriveKey( + nonce: HexKey, + onReady: (HexKey) -> Unit, + ) = launcher.launch( + DeriveKeyRequest.assemble(nonce, loggedInUser, packageName), + DeriveKeyResultProcessor(onReady), + ) +} From 909eb62db649ab9bebbfafbeeebc7541353c1ae5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Jul 2025 11:18:56 -0400 Subject: [PATCH 093/185] Moves private zap decryption to async, all at once. --- .../com/vitorpamplona/amethyst/model/Note.kt | 32 ++++++---- .../quartz/nip57Zaps/LnZapRequestEvent.kt | 4 +- .../quartz/utils/ParallelUtils.kt | 62 +++++++++++++++++++ 3 files changed, 86 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 372381c549..93820b9f1b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable +import coil3.util.CoilUtils.result import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji @@ -68,6 +69,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprov import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.anyAsync import com.vitorpamplona.quartz.utils.containsAny import com.vitorpamplona.quartz.utils.launchAndWaitAll import com.vitorpamplona.quartz.utils.tryAndWait @@ -496,6 +498,8 @@ open class Note( return } + val parallelDecrypt = mutableListOf>() + zapEvents.forEach { next -> val zapRequest = next.key.event as LnZapRequestEvent val zapEvent = next.value?.event as? LnZapEvent @@ -518,21 +522,27 @@ open class Note( } } else { if (account.isWriteable()) { - val result = - tryAndWait { continuation -> - zapRequest.decryptPrivateZap(account.signer) { - continuation.resume(it) - } - } - - if (result?.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) { - onWasZappedByAuthor() - return - } + parallelDecrypt.add(Pair(zapRequest, zapEvent)) } } } } + + val result = + anyAsync(parallelDecrypt) { pair -> + val result = + tryAndWait { continuation -> + pair.first.decryptPrivateZap(account.signer) { + continuation.resume(it) + } + } + + result?.pubKey == user.pubkeyHex && (option == null || option == pair.second?.zappedPollOption()) + } + + if (result) { + onWasZappedByAuthor() + } } suspend fun isZappedBy( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt index 851869d762..f2e00897ea 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt @@ -64,7 +64,9 @@ class LnZapRequestEvent( override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) - @Transient private var privateZapEvent: LnZapPrivateEvent? = null + // TODO: Create a per key map with resulting options to account for rejections and avoiding reasking. + @Transient + private var privateZapEvent: LnZapPrivateEvent? = null override fun countMemory(): Long = super.countMemory() + pointerSizeInBytes + (privateZapEvent?.countMemory() ?: 0) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt index 1d41584877..4e32f16a41 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt @@ -25,9 +25,11 @@ import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.joinAll +import kotlinx.coroutines.selects.select import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull import kotlin.coroutines.Continuation +import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume /** @@ -153,3 +155,63 @@ suspend fun mapNotNullAsync( } } } + +/** + * Executes a mapping function asynchronously on each input in the list. + * Returns true as soon as the first mapping returns true, cancelling all other ongoing operations. + * + * @param inputs A list of input objects to process. + * @param mappingFunction A suspend function that takes an input object and returns a Boolean. + * @return True if any mapping function returns true, false otherwise. + */ +suspend fun anyAsync( + inputs: List, + timeoutMillis: Long = 30000, + mappingFunction: suspend (T) -> Boolean, +): Boolean = + coroutineScope { + // Create a list to hold all our deferred results + val deferredResults = + inputs.map { input -> + async { + // Each async block will execute the mapping function. + // If this coroutine gets cancelled, CancellationException will be thrown, + // and we'll catch it to ensure it doesn't propagate further. + try { + mappingFunction(input) + } catch (e: CancellationException) { + // When cancelled, we treat it as if it didn't return true + false + } + } + } + + // Use select to wait for the first deferred to complete with 'true' + val foundTrue = + withTimeoutOrNull(timeoutMillis) { + select { + deferredResults.forEach { deferred -> + // For each deferred, if it completes and its result is 'true', + // this branch of the select expression will be chosen. + deferred.onAwait { result -> + if (result) { + true // Return true from the select expression + } else { + // If a deferred completes with false, we don't want to + // immediately end the select, so we return false, which + // lets select continue waiting for other branches. + false + } + } + } + } + } + + // Once select returns (either with true or after all deferreds complete/are cancelled), + // cancel any remaining ongoing operations. + // If foundTrue is true, all other deferreds are implicitly cancelled by the select winning. + // If foundTrue is false, it means all completed with false or were cancelled. + deferredResults.forEach { it.cancel() } // Ensure all are cancelled. + + return@coroutineScope foundTrue == true + } From 597fec5c4356ce03a0e1c1cf0f7ac5f3c53c20bb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Jul 2025 11:19:12 -0400 Subject: [PATCH 094/185] Removes lazy StateFlows due to inconsistent starting values. --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../edits/PrivateStorageRelayListState.kt | 5 ++-- .../model/emphChat/EphemeralChatListState.kt | 3 +-- .../FollowListOutboxRelays.kt | 25 +++++++++++-------- .../model/nip02FollowLists/FollowListState.kt | 1 + .../nip28PublicChats/PublicChatListState.kt | 8 +++--- .../model/nip30CustomEmojis/EmojiPackState.kt | 21 +++++++++++----- .../model/nip51Lists/GeohashListState.kt | 3 +-- .../model/nip51Lists/HashtagListState.kt | 3 +-- .../model/nip51Lists/HiddenUsersState.kt | 23 +++++++++-------- .../nip72Communities/CommunityListState.kt | 12 ++++----- .../amethyst/ui/screen/FollowListState.kt | 10 ++++++-- .../ui/screen/loggedIn/AccountViewModel.kt | 23 ++++++++++++++--- .../quartz/nip01Core/core/Event.kt | 2 +- .../nip01Core/relay/client/NostrClient.kt | 3 --- 15 files changed, 89 insertions(+), 55 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 92e400490b..0baa6cd1f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2266,7 +2266,7 @@ class Account( val oldHashtags = contactList?.hashtags()?.toSet() if (oldHashtags != null && oldHashtags.isNotEmpty()) { - Log.d("DB UPGRADE", "Migrating List with ${oldHashtags.size} old communities ") + Log.d("DB UPGRADE", "Migrating List with ${oldHashtags.size} old hashtags ") val existingHashtags = hashtagList.flow.value val needsToUpgrade = oldHashtags.filter { it !in existingHashtags } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt index 5c7510c8ca..43af1fdf90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt @@ -63,8 +63,9 @@ class PrivateStorageRelayListState( val flow = getPrivateOutboxRelayListFlow() .map { normalizePrivateOutboxRelayListWithBackup(it.note) } - .onStart { emit(normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote())) } - .flowOn(Dispatchers.Default) + .onStart { + emit(normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote())) + }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt index 1117e70fd8..07d22c6581 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt @@ -72,7 +72,7 @@ class EphemeralChatListState( } ?: emptySet() @OptIn(ExperimentalCoroutinesApi::class) - val liveEphemeralChatList: StateFlow> by lazy { + val liveEphemeralChatList: StateFlow> = getEphemeralChatListFlow() .transformLatest { noteState -> emit(ephemeralChatListWithBackup(noteState.note)) @@ -84,7 +84,6 @@ class EphemeralChatListState( SharingStarted.Eagerly, emptySet(), ) - } fun follow( channel: EphemeralChatChannel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt index 7bfeaa71b6..fde92f503c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt @@ -81,11 +81,14 @@ class FollowListOutboxRelays( emitAll(relayListFlows) }.onStart { val blocked = blockedRelayList.flow.value.toSet() - kind3Follows.flow.value.authors - .mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm()?.minus(blocked) - }.flatten() - .toSet() + val authors = kind3Follows.flow.value.authors + val perRelay = + authors + .mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm()?.minus(blocked) + }.flatten() + .toSet() + emit(perRelay) }.flowOn(Dispatchers.Default) .stateIn( scope, @@ -99,11 +102,13 @@ class FollowListOutboxRelays( .map { relayList -> relayList.map { it.url }.toSet() }.onStart { - kind3Follows.flow.value.authors - .mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() - }.flatten() - .toSet() + emit( + kind3Follows.flow.value.authors + .mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() + }.flatten() + .toSet(), + ) }.flowOn(Dispatchers.Default) .stateIn( scope, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt index e8d8c31822..59bedaa1e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt @@ -71,6 +71,7 @@ class FollowListState( .stateIn( scope, SharingStarted.Eagerly, + // this has priority. buildKind3Follows(getFollowListEvent() ?: settings.backupContactList), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt index 96e6693f21..109193c0ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt @@ -74,7 +74,7 @@ class PublicChatListState( } ?: emptySet() @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow> by lazy { + val flow: StateFlow> = getChannelListFlow() .transformLatest { noteState -> emit(publicChatListWithBackup(noteState.note)) @@ -86,20 +86,20 @@ class PublicChatListState( SharingStarted.Eagerly, emptySet(), ) - } @OptIn(ExperimentalCoroutinesApi::class) - val flowSet: StateFlow> by lazy { + val flowSet: StateFlow> = flow .map { it.mapTo(mutableSetOf()) { it.eventId } + }.onStart { + emit(flow.value.mapTo(mutableSetOf()) { it.eventId }) }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, emptySet(), ) - } fun follow( channel: PublicChatChannel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt index 9d43b89ec8..e81567ae9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest @@ -68,17 +69,18 @@ class EmojiPackState( } @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow>?> by lazy { + val flow: StateFlow>?> = getEmojiPackSelectionFlow() .transformLatest { emit(convertEmojiSelectionPack(it.note.event as? EmojiPackSelectionEvent)) + }.onStart { + emit(convertEmojiSelectionPack(getEmojiPackSelection())) }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, - convertEmojiSelectionPack(getEmojiPackSelection()), + emptyList(), ) - } fun convertEmojiPack(pack: EmojiPackEvent): List = pack.taggedEmojis().map { @@ -98,7 +100,7 @@ class EmojiPackState( .distinctBy { it.link } @OptIn(ExperimentalCoroutinesApi::class) - val myEmojis by lazy { + val myEmojis = flow .transformLatest { emojiList -> if (emojiList != null) { @@ -110,13 +112,20 @@ class EmojiPackState( } else { emit(emptyList()) } + }.onStart { + emit( + mergePack( + convertEmojiSelectionPack( + getEmojiPackSelection(), + )?.map { it.value }?.toTypedArray() ?: emptyArray(), + ), + ) }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, - mergePack(convertEmojiSelectionPack(getEmojiPackSelection())?.map { it.value }?.toTypedArray() ?: emptyArray()), + emptyList(), ) - } fun addEmojiPack( emojiPack: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt index 3d2e4e6ec2..096a55fca7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt @@ -70,7 +70,7 @@ class GeohashListState( } ?: emptySet() @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow> by lazy { + val flow: StateFlow> = getGeohashListFlow() .transformLatest { noteState -> emit(geohashListWithBackup(noteState.note)) @@ -82,7 +82,6 @@ class GeohashListState( SharingStarted.Eagerly, emptySet(), ) - } fun follow( geohashs: List, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt index 6b3e09b299..acf863a109 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt @@ -70,7 +70,7 @@ class HashtagListState( } ?: emptySet() @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow> by lazy { + val flow: StateFlow> = getHashtagListFlow() .transformLatest { noteState -> emit(hashtagListWithBackup(noteState.note)) @@ -82,7 +82,6 @@ class HashtagListState( SharingStarted.Eagerly, emptySet(), ) - } fun follow( hashtags: List, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt index 9888ba5e09..d0fd40434b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt @@ -33,9 +33,9 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update -import kotlinx.coroutines.runBlocking class HiddenUsersState( val muteList: StateFlow, @@ -73,7 +73,7 @@ class HiddenUsersState( showSensitiveContent = showSensitiveContent, ) - val flow: StateFlow by lazy { + val flow: StateFlow = combineTransform( blockList, muteList, @@ -82,20 +82,21 @@ class HiddenUsersState( ) { blockList, muteList, transientHiddenUsers, showSensitiveContent -> checkNotInMainThread() emit(assembleLiveHiddenUsers(blockList, muteList, transientHiddenUsers, showSensitiveContent)) + }.onStart { + emit( + assembleLiveHiddenUsers( + blockList.value, + muteList.value, + transientHiddenUsers.value, + settings.syncedSettings.security.showSensitiveContent.value, + ), + ) }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, - runBlocking { - assembleLiveHiddenUsers( - blockList.value, - muteList.value, - transientHiddenUsers.value, - settings.syncedSettings.security.showSensitiveContent.value, - ) - }, + LiveHiddenUsers(emptySet(), emptySet(), emptySet(), null), ) - } fun resetTransientUsers() { transientHiddenUsers.update { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt index 84f1271553..2690fb53be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt @@ -73,7 +73,7 @@ class CommunityListState( } ?: emptySet() @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow> by lazy { + val flow: StateFlow> = getCommunityListFlow() .transformLatest { noteState -> emit(communityListWithBackup(noteState.note)) @@ -85,20 +85,20 @@ class CommunityListState( SharingStarted.Eagerly, emptySet(), ) - } @OptIn(ExperimentalCoroutinesApi::class) - val flowSet: StateFlow> by lazy { + val flowSet: StateFlow> = flow - .map { - it.mapTo(mutableSetOf()) { it.addressId } + .map { hint -> + hint.mapTo(mutableSetOf()) { it.addressId } + }.onStart { + emit(flow.value.mapTo(mutableSetOf()) { it.addressId }) }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, emptySet(), ) - } fun follow( communities: List, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt index 11203eddf6..b55718b840 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt @@ -237,8 +237,14 @@ class FollowListState( ) } - val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists) - val kind3GlobalPeople = _kind3GlobalPeople.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists) + val kind3GlobalPeopleRoutes = + _kind3GlobalPeopleRoutes + .flowOn(Dispatchers.Default) + .stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists) + val kind3GlobalPeople = + _kind3GlobalPeople + .flowOn(Dispatchers.Default) + .stateIn(viewModelScope, SharingStarted.Eagerly, defaultLists) suspend fun initializeSuspend() { checkNotInMainThread() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index b919be4d90..024aab78d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -194,7 +194,10 @@ class AccountViewModel( emit(newestItemCreatedAt != null && newestItemCreatedAt > lastRead) } - val notificationHasNewItemsFlow = notificationHasNewItems.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, false) + val notificationHasNewItemsFlow = + notificationHasNewItems + .flowOn(Dispatchers.Default) + .stateIn(viewModelScope, SharingStarted.Eagerly, false) @OptIn(ExperimentalCoroutinesApi::class) val messagesHasNewItems = @@ -225,7 +228,10 @@ class AccountViewModel( } } - val messagesHasNewItemsFlow = messagesHasNewItems.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, false) + val messagesHasNewItemsFlow = + messagesHasNewItems + .flowOn(Dispatchers.Default) + .stateIn(viewModelScope, SharingStarted.Eagerly, false) @OptIn(ExperimentalCoroutinesApi::class) val homeHasNewItems = @@ -245,7 +251,10 @@ class AccountViewModel( emit(newestItemCreatedAt != null && newestItemCreatedAt > lastRead) } - val homeHasNewItemsFlow = homeHasNewItems.flowOn(Dispatchers.Default).stateIn(viewModelScope, SharingStarted.Eagerly, false) + val homeHasNewItemsFlow = + homeHasNewItems + .flowOn(Dispatchers.Default) + .stateIn(viewModelScope, SharingStarted.Eagerly, false) val hasNewItems = mapOf( @@ -359,6 +368,14 @@ class AccountViewModel( note.flow().reports.stateFlow, ) { hiddenUsers, followingUsers, autor, metadata, reports -> emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.authors)) + }.onStart { + emit( + isNoteAcceptable( + note, + account.hiddenUsers.flow.value, + account.kind3FollowList.flow.value.authors, + ), + ) }.flowOn(Dispatchers.Default) .stateIn( viewModelScope, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt index cea5d7867a..806b499961 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt @@ -63,7 +63,7 @@ open class Event( try { fromJson(json) } catch (e: Exception) { - Log.e("Event", "Unable to parse event JSON: $json", e) + Log.w("Event", "Unable to parse event JSON: $json", e) null } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index 7371deaae2..e2b587d9b1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -41,7 +41,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.stateIn @@ -93,8 +92,6 @@ class NostrClient( eventOutbox.relays, ) { reqs, counts, outbox -> reqs + counts + outbox - }.onStart { - activeRequests.relays.value + activeCounts.relays.value + eventOutbox.relays.value }.sample(300) .onEach { relayPool.updatePool(it) From 850277fdc1b4e8d02dc301d536d87113870fb1c7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Jul 2025 11:29:21 -0400 Subject: [PATCH 095/185] Optimize imports --- .../vitorpamplona/amethyst/model/Account.kt | 7 ------- .../com/vitorpamplona/amethyst/model/Note.kt | 1 - .../AccountOutboxRelayState.kt | 1 - .../FollowListOutboxRelays.kt | 4 ---- .../nip02FollowLists/FollowsPerOutboxRelay.kt | 1 - .../model/nip56Reports/ReportAction.kt | 1 - .../model/topNavFeeds/CommunityRelayLoader.kt | 2 -- .../model/topNavFeeds/OutboxRelayLoader.kt | 1 - .../AllCommunitiesTopNavFilter.kt | 1 - .../service/Nip11RelayInfoRetriever.kt | 1 - .../service/okhttp/OkHttpWebSocket.kt | 1 - .../eoseManagers/PerUniqueIdEoseManager.kt | 1 - .../PerUserAndFollowListEoseManager.kt | 1 - .../eoseManagers/PerUserEoseManager.kt | 1 - .../eoseManagers/SingleSubEoseManager.kt | 1 - .../SingleSubNoEoseCacheEoseManager.kt | 1 - .../metadata/AccountMetadataEoseManager.kt | 1 - .../AccountNotificationsEoseManager.kt | 1 - .../AccountGiftWrapsEoseManager.kt | 1 - .../loaders/FilterMissingAddressables.kt | 1 - .../user/loaders/FilterUserMetadataForKey.kt | 1 - .../user/watchers/UserReportsSubAssembler.kt | 1 - .../user/watchers/UserWatcherSubAssembler.kt | 1 - .../subassemblies/FilterByAddress.kt | 2 -- .../subassemblies/FilterByEvent.kt | 2 -- .../amethyst/service/relays/EOSE.kt | 1 - .../amethyst/ui/actions/MediaSaverToDisk.kt | 2 -- .../amethyst/ui/components/MyAsyncImage.kt | 3 --- .../ui/feeds/ChannelFeedContentState.kt | 1 - .../ui/note/creators/previews/PreviewUrl.kt | 1 - .../nip22Comments/CommentPostViewModel.kt | 1 - .../privateDM/datasource/FilterNip04DMs.kt | 2 -- .../DMsFromUserFilterSubAssembler.kt | 1 - .../FollowingEphemeralChatSubAssembler.kt | 1 - .../FollowingPublicChatSubAssembler.kt | 1 - .../CommunityFeedFilterSubAssembler.kt | 1 - .../FilterLongFormByAllCommunities.kt | 1 - .../subassemblies/FilterLongFormByAuthors.kt | 2 -- .../FilterLongFormByCommunity.kt | 1 - .../subassemblies/FilterLongFormByFollows.kt | 1 - .../subassemblies/FilterLongFormByGeohash.kt | 1 - .../subassemblies/FilterLongFormByHashtag.kt | 1 - .../FilterPublicChatsByAllCommunities.kt | 1 - .../FilterPublicChatsByAuthors.kt | 2 -- .../FilterPublicChatsByCommunity.kt | 1 - .../FilterPublicChatsByFollows.kt | 1 - .../FilterPublicChatsByGeohash.kt | 1 - .../FilterPublicChatsByHashtag.kt | 1 - .../FilterFollowSetsByAllCommunities.kt | 1 - .../FilterFollowSetsByAuthors.kt | 2 -- .../FilterFollowSetsByCommunity.kt | 1 - .../FilterFollowSetsByFollows.kt | 1 - .../FilterFollowSetsByGeohash.kt | 1 - .../FilterFollowSetsByHashtag.kt | 1 - .../FilterLiveActivitiesByAllCommunities.kt | 1 - .../FilterLiveActivitiesByAuthors.kt | 2 -- .../FilterLiveActivitiesByCommunity.kt | 1 - .../FilterLiveActivitiesByFollows.kt | 1 - .../FilterLiveActivitiesByGeohash.kt | 1 - .../FilterLiveActivitiesByHashtag.kt | 1 - .../FilterCommunitiesByAllCommunities.kt | 1 - .../FilterCommunitiesByAuthors.kt | 2 -- .../FilterCommunitiesByCommunity.kt | 1 - .../FilterCommunitiesByFollows.kt | 1 - .../FilterCommunitiesByGeohash.kt | 1 - .../FilterCommunitiesByHashtag.kt | 1 - .../FilterContentDVMsByAllCommunities.kt | 1 - .../FilterContentDVMsByAuthors.kt | 2 -- .../FilterContentDVMsByCommunity.kt | 1 - .../FilterContentDVMsByFollows.kt | 1 - .../FilterContentDVMsByGeohash.kt | 1 - .../FilterContentDVMsByHashtag.kt | 1 - .../FilterClassifiedsByAllCommunities.kt | 1 - .../FilterClassifiedsByAuthors.kt | 2 -- .../FilterClassifiedsByCommunity.kt | 1 - .../FilterClassifiedsByFollows.kt | 1 - .../FilterClassifiedsByGeohash.kt | 1 - .../FilterClassifiedsByHashtag.kt | 1 - .../loggedIn/home/dal/HomeLiveFilter.kt | 1 - .../nip01Core/FilterHomePostsByGeohashes.kt | 3 --- .../nip01Core/FilterHomePostsByHashtags.kt | 2 -- .../FilterHomePostsByAllFollows.kt | 1 - .../nip65Follows/FilterHomePostsByAuthors.kt | 1 - .../FilterHomePostsByAllCommunities.kt | 1 - .../FilterHomePostsFromCommunities.kt | 2 -- .../UserProfileMetadataFilterSubAssembler.kt | 1 - .../UserProfileZapsFilterSubAssembler.kt | 1 - .../common/BasicRelaySetupInfoClickableRow.kt | 1 - .../FilterMissingEventsForThread.kt | 2 -- .../FilterPictureAndVideoByGeohash.kt | 2 -- .../FilterPictureAndVideoByHashtag.kt | 1 - .../FilterPictureAndVideoByAuthors.kt | 2 -- .../FilterPictureAndVideoByFollows.kt | 1 - .../FilterPictureAndVideoByAllCommunities.kt | 1 - .../FilterPictureAndVideoByCommunity.kt | 1 - .../screen/loggedOff/signup/SignUpScreen.kt | 2 -- .../src/main/res/values-cy-rGB/strings.xml | 2 +- .../src/main/res/values-da-rDK/strings.xml | 2 +- .../src/main/res/values-en-rGB/strings.xml | 2 +- .../src/main/res/values-fr-rCA/strings.xml | 2 +- .../src/main/res/values-gu-rIN/strings.xml | 2 +- .../src/main/res/values-hr-rHR/strings.xml | 2 +- .../src/main/res/values-iw-rIL/strings.xml | 2 +- .../src/main/res/values-kk-rKZ/strings.xml | 2 +- .../src/main/res/values-ko-rKR/strings.xml | 2 +- .../src/main/res/values-ks-rIN/strings.xml | 2 +- .../src/main/res/values-lt-rLT/strings.xml | 2 +- .../src/main/res/values-lv-rLV/strings.xml | 2 +- .../src/main/res/values-ne-rNP/strings.xml | 2 +- .../src/main/res/values-pcm-rNG/strings.xml | 2 +- .../src/main/res/values-pt-rPT/strings.xml | 2 +- .../src/main/res/values-ru-rUA/strings.xml | 2 +- .../src/main/res/values-sa-rIN/strings.xml | 2 +- .../src/main/res/values-sr-rSP/strings.xml | 2 +- .../src/main/res/values-sw-rTZ/strings.xml | 2 +- .../src/main/res/values-tr-rTR/strings.xml | 2 +- amethyst/src/main/res/values-tr/strings.xml | 2 +- .../src/main/res/values-uz-rUZ/strings.xml | 2 +- .../src/main/res/values-vi-rVN/strings.xml | 2 +- .../src/main/res/values-zh-rSG/strings.xml | 2 +- .../nip22Comments/GeolocatedComments.kt | 1 + .../ephemChat/list/EphemeralChatListEvent.kt | 1 - .../experimental/zapPolls/PollNoteEvent.kt | 5 ----- .../quartz/nip01Core/core/TagArray.kt | 2 -- .../quartz/nip01Core/jackson/JsonMapper.kt | 1 - .../relay/client/pool/PoolEventOutbox.kt | 1 - .../nip01Core/relay/client/pool/RelayPool.kt | 3 --- .../client/single/basic/BasicRelayClient.kt | 1 + .../relay/normalizer/RelayUrlNormalizer.kt | 1 - .../quartz/nip01Core/tags/events/ETag.kt | 1 - .../nip02FollowList/ContactListEvent.kt | 3 --- .../quartz/nip10Notes/BaseThreadedEvent.kt | 1 - .../quartz/nip10Notes/TextNoteEvent.kt | 5 ----- .../quartz/nip10Notes/tags/MarkedETag.kt | 2 -- .../quartz/nip18Reposts/RepostEvent.kt | 1 - .../quartz/nip19Bech32/ListEntityExt.kt | 2 -- .../quartz/nip22Comments/CommentEvent.kt | 5 ----- .../nip23LongContent/LongTextNoteEvent.kt | 1 - .../admin/ChannelCreateEvent.kt | 2 -- .../nip28PublicChat/list/ChannelListEvent.kt | 1 - .../message/ChannelMessageEvent.kt | 5 ----- .../quartz/nip34Git/issue/GitIssueEvent.kt | 1 - .../quartz/nip34Git/patch/GitPatchEvent.kt | 2 -- .../quartz/nip34Git/reply/GitReplyEvent.kt | 1 - .../nip35Torrents/TorrentCommentEvent.kt | 1 - .../quartz/nip37Drafts/DraftEvent.kt | 3 --- .../nip51Lists/locations/GeohashListEvent.kt | 1 - .../streaming/LiveActivitiesEvent.kt | 1 - .../api/background/queries/DecryptZapQuery.kt | 1 - .../api/foreground/IntentRequestDatabase.kt | 1 - .../api/foreground/NewResultProcessor.kt | 20 ------------------- .../quartz/nip57Zaps/LnZapEvent.kt | 4 ---- .../quartz/nip57Zaps/LnZapPrivateEvent.kt | 3 --- .../quartz/nip58Badges/BadgeAwardEvent.kt | 3 --- .../quartz/nip58Badges/BadgeProfilesEvent.kt | 2 -- .../AdvertisedRelayListEvent.kt | 1 - .../tags/AdvertisedRelayInfoTag.kt | 1 - .../approval/CommunityPostApprovalEvent.kt | 7 ------- .../definition/CommunityDefinitionEvent.kt | 7 ------- .../follow/CommunityListEvent.kt | 3 --- .../quartz/nip73ExternalIds/movies/MovieId.kt | 20 ------------------- .../nip73ExternalIds/topics/HashtagId.kt | 20 ------------------- .../quartz/nip75ZapGoals/GoalEvent.kt | 3 --- .../quartz/nip84Highlights/HighlightEvent.kt | 5 ----- .../definition/AppDefinitionEvent.kt | 1 - .../vitorpamplona/quartz/utils/LargeCache.kt | 2 -- .../quartz/utils/MapOfSetBuilder.kt | 2 -- .../quartz/utils/RandomInstance.kt | 1 - 168 files changed, 26 insertions(+), 311 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 0baa6cd1f7..7460795c9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -209,13 +209,6 @@ import kotlinx.coroutines.launch import java.math.BigDecimal import java.util.Base64 import java.util.Locale -import kotlin.Boolean -import kotlin.collections.filter -import kotlin.collections.flatten -import kotlin.collections.ifEmpty -import kotlin.collections.map -import kotlin.collections.plus -import kotlin.collections.toSet import kotlin.coroutines.resume @OptIn(DelicateCoroutinesApi::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 93820b9f1b..4594fa3402 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import coil3.util.CoilUtils.result import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt index 8db6d0c8d4..001e086ac2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn -import kotlin.collections.plus class AccountOutboxRelayState( nip65: Nip65RelayListState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt index fde92f503c..bf6a4e37ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt @@ -40,10 +40,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlin.collections.flatten -import kotlin.collections.map -import kotlin.collections.minus -import kotlin.collections.toSet class FollowListOutboxRelays( kind3Follows: FollowListState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt index 204ae9a6fa..00603939a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -40,7 +40,6 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlin.collections.map class FollowsPerOutboxRelay( kind3Follows: FollowListState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt index bdec0ec8cf..09bcc225f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.model.nip56Reports -import android.R.attr.type import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt index a49b4db544..d4f4e44fea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/CommunityRelayLoader.kt @@ -28,8 +28,6 @@ import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefiniti import com.vitorpamplona.quartz.utils.mapOfSet import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlin.collections.forEach -import kotlin.collections.ifEmpty class CommunityRelayLoader { companion object { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt index 7f784d533b..26bee7d8e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/OutboxRelayLoader.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.mapOfSet import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlin.collections.ifEmpty class OutboxRelayLoader { companion object { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt index 57df0edcc9..40debbe42d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/allcommunities/AllCommunitiesTopNavFilter.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableN import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlin.collections.minus @Immutable class AllCommunitiesTopNavFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt index 3c33640931..d58d29dd35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/Nip11RelayInfoRetriever.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.service import android.util.Log import android.util.LruCache -import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.toHttp diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt index 00ec01942b..a70bbbc7c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.service.okhttp -import android.system.Os.socket import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt index e8a9292c15..dfb326b121 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUniqueIdEoseManager.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlin.collections.distinctBy /** * This query type creates a new relay subscription for every SubID.id() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt index 16fe9ae1d0..047238bb00 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserAndFollowListEoseManager.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlin.collections.distinctBy /** * This query type creates a new relay subscription for every logged-in diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt index 65e17aa7b5..d0d4b4e672 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/PerUserEoseManager.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlin.collections.distinctBy /** * This query type creates a new relay subscription for every distinct diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt index 443f034aca..4ed8dbbbc9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubEoseManager.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlin.collections.distinctBy /** * This query type creates only ONE relay subscription. It filters duplicates diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt index bd03a71451..651cc38dd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/eoseManagers/SingleSubNoEoseCacheEoseManager.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.service.relayClient.eoseManagers import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.groupByRelay -import kotlin.collections.distinctBy /** * This query type creates only ONE relay subscription and does not diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt index aacd90b3b5..cba60b2596 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/AccountMetadataEoseManager.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlin.collections.forEach class AccountMetadataEoseManager( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt index 3556bedc83..331080ef65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlin.collections.forEach class AccountNotificationsEoseManager( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt index 048e9b1e2c..79928cd57d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip59GiftWraps/AccountGiftWrapsEoseManager.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlin.collections.forEach class AccountGiftWrapsEoseManager( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt index d37c23dab7..5beab5ac6f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/loaders/FilterMissingAddressables.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCache.relayHints import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt index b2b659438e..d147ef3fff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/loaders/FilterUserMetadataForKey.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.mapOfSet -import kotlin.collections.forEach val MetadataAndRelayListKinds = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt index acbde3be27..d90135544f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserReportsSubAssembler.kt @@ -30,7 +30,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet -import kotlin.collections.flatten class UserReportsSubAssembler( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt index 7102a33eb7..1c9cfb4779 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/watchers/UserWatcherSubAssembler.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.ammolite.relays.filters.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlin.collections.flatten class UserWatcherSubAssembler( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt index 7e7320fcbb..7b4ee76089 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByAddress.kt @@ -27,8 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.utils.mapOfSet -import kotlin.collections.forEach -import kotlin.collections.ifEmpty fun filterByAddress( address: NAddress, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt index e69b20dad2..5401aeaa13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/FilterByEvent.kt @@ -28,8 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet -import kotlin.collections.forEach -import kotlin.collections.ifEmpty fun filterByEvent( eventId: HexKey, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt index 7c56420caf..4e529baac1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relays/EOSE.kt @@ -24,7 +24,6 @@ import androidx.collection.LruCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.ammolite.relays.filters.MutableTime import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlin.collections.plus typealias SincePerRelayMap = Map diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt index 42773706c8..5099ff3647 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt @@ -32,9 +32,7 @@ import android.webkit.MimeTypeMap import androidx.annotation.RequiresApi import androidx.core.net.toFile import androidx.core.net.toUri -import coil3.util.MimeTypeMap.getMimeTypeFromExtension import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk.PICTURES_SUBDIRECTORY -import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt index ef08d9374d..9effe29c4e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.components -import android.R.attr.contentDescription import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.size @@ -36,7 +35,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import coil3.compose.AsyncImagePainter -import coil3.compose.AsyncImagePainter.State.Empty.painter import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImageContent import com.vitorpamplona.amethyst.model.MediaAspectRatioCache @@ -46,7 +44,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.Size6dp import com.vitorpamplona.amethyst.ui.theme.Size75dp -import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError @Composable fun MyAsyncImage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt index cb6998bd5a..ae0cfcf0b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt @@ -38,7 +38,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import kotlin.collections.distinctBy @Stable class ChannelFeedContentState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt index a0783d262e..7be2f5204f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt @@ -53,7 +53,6 @@ import com.vitorpamplona.amethyst.ui.components.WaitAndDisplay import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import kotlin.text.startsWith @Composable fun PreviewUrl( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 9750c6d93c..27218654cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -104,7 +104,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlin.collections.plus import kotlin.coroutines.resume @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt index e36eb8247a..7fac8d0bc4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/datasource/FilterNip04DMs.kt @@ -29,8 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import kotlin.collections.ifEmpty -import kotlin.collections.toList fun filterNip04DMs( group: Set?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt index 8c7df6be70..8463cab688 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/DMsFromUserFilterSubAssembler.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlin.collections.forEach class DMsFromUserFilterSubAssembler( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt index e4014d47f6..3c46a38764 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingEphemeralChatSubAssembler.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch -import kotlin.collections.forEach class FollowingEphemeralChatSubAssembler( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt index e17a7a4716..a59c0fdc5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/datasource/FollowingPublicChatSubAssembler.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch -import kotlin.collections.forEach class FollowingPublicChatSubAssembler( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt index bb630d892d..bd7db1129b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/communities/datasource/CommunityFeedFilterSubAssembler.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import kotlin.collections.ifEmpty class CommunityFeedFilterSubAssembler( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt index e7aedaf0b9..b1ac3ea3fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import kotlin.collections.flatten fun filterLongFormAllCommunities( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt index 73355dc066..eb58737988 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt @@ -28,8 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import kotlin.collections.flatten -import kotlin.collections.mapNotNull fun filterLongFormAuthors( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt index bd57e55d9e..55553bcce1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import kotlin.collections.flatten fun filterLongFormByCommunity( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt index ea986583c3..e86af94c60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.sub import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import kotlin.collections.flatMap fun filterLongFormByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt index b0bedecce1..6764bf8a9e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import kotlin.collections.mapNotNull fun filterLongFormByGeohash( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt index e197ce8c2e..a9c47e112a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import kotlin.collections.mapNotNull fun filterLongFormByHashtag( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt index 518dfe54c3..7b3092f74c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import kotlin.collections.flatten fun filterPublicChatsAllCommunities( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt index c40160aecd..aea997e2c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt @@ -30,8 +30,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import kotlin.collections.flatten -import kotlin.collections.mapNotNull fun filterPublicChatsAuthors( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt index 690ae70b11..6976a0940d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import kotlin.collections.flatten fun filterPublicChatsByCommunity( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt index c71864df9f..6da7b58e9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subass import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import kotlin.collections.flatMap fun filterPublicChatsByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt index f41fe12280..1804ef29ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import kotlin.collections.mapNotNull fun filterPublicChatsByGeohash( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt index f71ecdb885..2138760b00 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent -import kotlin.collections.mapNotNull fun filterPublicChatsByHashtag( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt index eb8c9bc958..7616138712 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import kotlin.collections.flatten fun filterFollowSetsAllCommunities( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt index 032303bf30..794c18d8aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt @@ -28,8 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import kotlin.collections.flatten -import kotlin.collections.mapNotNull fun filterFollowSetsAuthors( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt index bf7f27e293..cb38081b8a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.FollowListEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import kotlin.collections.flatten fun filterFollowSetsByCommunity( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt index cdc321d1e5..6dcb143370 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.s import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import kotlin.collections.flatMap fun filterFollowSetsByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt index 5f27987d25..578a766d25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import kotlin.collections.mapNotNull fun filterFollowSetsByGeohash( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt index 93d638c066..8e330a8625 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import kotlin.collections.mapNotNull fun filterFollowSetsByHashtag( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt index 1d1d7b2d76..3dc411bc4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import kotlin.collections.flatten fun filterLiveActivitiesAllCommunities( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt index 7bbb51135f..22d03b22ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt @@ -29,8 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import kotlin.collections.flatten -import kotlin.collections.mapNotNull fun filterLiveActivitiesAuthors( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt index dc4499e277..c87e918806 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import kotlin.collections.flatten fun filterLiveActivitiesByCommunity( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt index bbce0a5227..1e8cbb58bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActiviti import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import kotlin.collections.flatMap fun filterLiveActivitiesByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt index fc0c789348..62471f337f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import kotlin.collections.mapNotNull fun filterLiveActivitiesByGeohash( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt index 9d8be1f461..5193a653e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import kotlin.collections.mapNotNull fun filterLiveActivitiesByHashtag( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt index 7920bfb56b..da3004e2d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import kotlin.collections.flatten fun filterCommunitiesAllCommunities( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt index 44f4fbd0d4..6069271358 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt @@ -29,8 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import kotlin.collections.flatten -import kotlin.collections.mapNotNull fun filterCommunitiesAuthors( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt index 062a4b5a75..92be0c095e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import kotlin.collections.flatten fun filterClassifiedsByCommunity( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt index b4719b5abc..7d233eaefb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities. import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import kotlin.collections.flatMap fun filterCommunitiesByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt index 1f3bf415c2..513b738ecc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import kotlin.collections.mapNotNull fun filterCommunitiesByGeohash( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt index 2e88e90a8b..bc439a1d01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent -import kotlin.collections.mapNotNull fun filterCommunitiesByHashtag( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt index 43bf3702fb..70ae780cd3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import kotlin.collections.flatten fun filterContentDVMsAllCommunities( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt index bf5f9eec91..25c3a031ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt @@ -28,8 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import kotlin.collections.flatten -import kotlin.collections.mapNotNull fun filterContentDVMsAuthors( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt index 1413d5a106..ed61e54797 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import kotlin.collections.flatten fun filterContentDVMsByCommunity( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt index cf5c2527f1..82fff15cb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subasse import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import kotlin.collections.flatMap fun filterContentDVMsByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt index dad6a5ea53..871e96151c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import kotlin.collections.mapNotNull fun filterContentDVMsByGeohash( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt index 876a9bf3e1..9150045172 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import kotlin.collections.mapNotNull fun filterContentDVMsByHashtag( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt index 5d372b2bc9..67d713ec33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import kotlin.collections.flatten fun filterClassifiedsAllCommunities( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt index 7b399a45f7..a1012c980b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt @@ -28,8 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import kotlin.collections.flatten -import kotlin.collections.mapNotNull fun filterClassifiedsAuthors( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt index 77a50c174d..5645e2c466 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import kotlin.collections.flatten fun filterClassifiedsByCommunity( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt index b8613876e3..4d84d570df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds. import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import kotlin.collections.flatten fun filterClassifiedsByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt index e0a5b7756a..2d1125f7b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import kotlin.collections.mapNotNull fun filterClassifiedsByGeohash( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt index f9d68f0469..bb30a6f333 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import kotlin.collections.mapNotNull fun filterClassifiedsByHashtag( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 7da2420821..75f5a2d510 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -33,7 +33,6 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.utils.TimeUtils -import kotlin.collections.toSet class HomeLiveFilter( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt index 8d14c4423f..0bf255dce7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt @@ -35,9 +35,6 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import kotlin.collections.flatten -import kotlin.collections.mapNotNull -import kotlin.collections.sorted val HomePostsByGeohashKinds = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt index 602d2aca3d..5ee8386b3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt @@ -37,8 +37,6 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import kotlin.collections.flatten -import kotlin.collections.mapNotNull val HomePostsBuHashtagsKinds = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt index 695b70236d..03d158a668 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.f import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Communities.filterHomePostsFromAllCommunities import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import kotlin.collections.flatten fun filterHomePostsByAllFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt index a823073eeb..dcc8c45442 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -42,7 +42,6 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEven import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import kotlin.collections.mapNotNull val HomePostsKinds = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt index 9376353807..14cf1c6758 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import kotlin.collections.flatten fun filterHomePostsFromAllCommunities( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt index 7ff8d1db43..358b05eef8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt @@ -33,8 +33,6 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import kotlin.collections.flatten -import kotlin.collections.mapNotNull val HomePostsFromCommunityKinds = listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt index 2ed12bc1d2..0b028e7410 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.utils.mapOfSet -import kotlin.collections.flatten class UserProfileMetadataFilterSubAssembler( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt index 6e2a89595b..04808d7247 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileZapsFilterSubAssembler.kt @@ -24,7 +24,6 @@ import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseMa import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import kotlin.collections.flatten class UserProfileZapsFilterSubAssembler( client: NostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt index a799bdf1ee..98da2d9219 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common -import android.R.attr.onClick import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt index 791cadcdfc..8e0ad15d1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/datasources/subassembies/FilterMissingEventsForThread.kt @@ -29,8 +29,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.loaders.p import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet -import kotlin.collections.forEach -import kotlin.collections.ifEmpty fun filterMissingEventsForThread( threadInfo: ThreadAssembler.ThreadInfo, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt index 5e8d242fab..b0915ad517 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt @@ -28,8 +28,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAnd import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlin.collections.flatten -import kotlin.collections.mapNotNull fun filterPictureAndVideoGeohash( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt index 259a1470f3..068f4869f7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts -import kotlin.collections.flatten fun filterPictureAndVideoHashtag( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt index 3ba1d739e5..be5c39ef4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt @@ -30,8 +30,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import kotlin.collections.flatten -import kotlin.collections.mapNotNull fun filterPictureAndVideoAuthors( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt index 55bb3deae6..4264ab6cc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassembl import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoHashtag import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip72Communities.filterPictureAndVideoAllCommunities import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter -import kotlin.collections.flatten fun filterPictureAndVideoByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt index 997bdb254c..1db6d48f68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent -import kotlin.collections.flatten fun filterPictureAndVideoAllCommunities( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt index 2dca47d3fd..11b0f7c584 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent -import kotlin.collections.flatten fun filterPictureAndVideoCommunity( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt index ca7c3e8045..cbb8f530f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/signup/SignUpScreen.kt @@ -39,9 +39,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale diff --git a/amethyst/src/main/res/values-cy-rGB/strings.xml b/amethyst/src/main/res/values-cy-rGB/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-cy-rGB/strings.xml +++ b/amethyst/src/main/res/values-cy-rGB/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-da-rDK/strings.xml b/amethyst/src/main/res/values-da-rDK/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-da-rDK/strings.xml +++ b/amethyst/src/main/res/values-da-rDK/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-en-rGB/strings.xml b/amethyst/src/main/res/values-en-rGB/strings.xml index 2e780a1344..48dcf6594e 100644 --- a/amethyst/src/main/res/values-en-rGB/strings.xml +++ b/amethyst/src/main/res/values-en-rGB/strings.xml @@ -1,5 +1,5 @@ - + Point to the QR Code Show QR Profile Image diff --git a/amethyst/src/main/res/values-fr-rCA/strings.xml b/amethyst/src/main/res/values-fr-rCA/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-fr-rCA/strings.xml +++ b/amethyst/src/main/res/values-fr-rCA/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-gu-rIN/strings.xml b/amethyst/src/main/res/values-gu-rIN/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-gu-rIN/strings.xml +++ b/amethyst/src/main/res/values-gu-rIN/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-hr-rHR/strings.xml b/amethyst/src/main/res/values-hr-rHR/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-hr-rHR/strings.xml +++ b/amethyst/src/main/res/values-hr-rHR/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-iw-rIL/strings.xml b/amethyst/src/main/res/values-iw-rIL/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-iw-rIL/strings.xml +++ b/amethyst/src/main/res/values-iw-rIL/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-kk-rKZ/strings.xml b/amethyst/src/main/res/values-kk-rKZ/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-kk-rKZ/strings.xml +++ b/amethyst/src/main/res/values-kk-rKZ/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-ko-rKR/strings.xml b/amethyst/src/main/res/values-ko-rKR/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-ko-rKR/strings.xml +++ b/amethyst/src/main/res/values-ko-rKR/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-ks-rIN/strings.xml b/amethyst/src/main/res/values-ks-rIN/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-ks-rIN/strings.xml +++ b/amethyst/src/main/res/values-ks-rIN/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-lt-rLT/strings.xml b/amethyst/src/main/res/values-lt-rLT/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-lt-rLT/strings.xml +++ b/amethyst/src/main/res/values-lt-rLT/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-lv-rLV/strings.xml b/amethyst/src/main/res/values-lv-rLV/strings.xml index 107dafa9f3..b92786e71f 100644 --- a/amethyst/src/main/res/values-lv-rLV/strings.xml +++ b/amethyst/src/main/res/values-lv-rLV/strings.xml @@ -1,5 +1,5 @@ - + Profila attēls Jūsu profila attēls 👀 diff --git a/amethyst/src/main/res/values-ne-rNP/strings.xml b/amethyst/src/main/res/values-ne-rNP/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-ne-rNP/strings.xml +++ b/amethyst/src/main/res/values-ne-rNP/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-pcm-rNG/strings.xml b/amethyst/src/main/res/values-pcm-rNG/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-pcm-rNG/strings.xml +++ b/amethyst/src/main/res/values-pcm-rNG/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-pt-rPT/strings.xml b/amethyst/src/main/res/values-pt-rPT/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-pt-rPT/strings.xml +++ b/amethyst/src/main/res/values-pt-rPT/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-ru-rUA/strings.xml b/amethyst/src/main/res/values-ru-rUA/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-ru-rUA/strings.xml +++ b/amethyst/src/main/res/values-ru-rUA/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-sa-rIN/strings.xml b/amethyst/src/main/res/values-sa-rIN/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-sa-rIN/strings.xml +++ b/amethyst/src/main/res/values-sa-rIN/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-sr-rSP/strings.xml b/amethyst/src/main/res/values-sr-rSP/strings.xml index 25197aa216..4e434e5ce6 100644 --- a/amethyst/src/main/res/values-sr-rSP/strings.xml +++ b/amethyst/src/main/res/values-sr-rSP/strings.xml @@ -1,5 +1,5 @@ - + Покажите на КР код Прикажи КР слика профила diff --git a/amethyst/src/main/res/values-sw-rTZ/strings.xml b/amethyst/src/main/res/values-sw-rTZ/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-sw-rTZ/strings.xml +++ b/amethyst/src/main/res/values-sw-rTZ/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/amethyst/src/main/res/values-tr-rTR/strings.xml b/amethyst/src/main/res/values-tr-rTR/strings.xml index 35621aa3a6..b404a3f538 100644 --- a/amethyst/src/main/res/values-tr-rTR/strings.xml +++ b/amethyst/src/main/res/values-tr-rTR/strings.xml @@ -1,5 +1,5 @@ - + QR Code\'a yönlendir QR Göster Profil Resmi diff --git a/amethyst/src/main/res/values-tr/strings.xml b/amethyst/src/main/res/values-tr/strings.xml index 35621aa3a6..b404a3f538 100644 --- a/amethyst/src/main/res/values-tr/strings.xml +++ b/amethyst/src/main/res/values-tr/strings.xml @@ -1,5 +1,5 @@ - + QR Code\'a yönlendir QR Göster Profil Resmi diff --git a/amethyst/src/main/res/values-uz-rUZ/strings.xml b/amethyst/src/main/res/values-uz-rUZ/strings.xml index cc4646375a..49538bb471 100644 --- a/amethyst/src/main/res/values-uz-rUZ/strings.xml +++ b/amethyst/src/main/res/values-uz-rUZ/strings.xml @@ -1,5 +1,5 @@ - + Point to the QR Code QR kodni ko‘rsatish Profil rasmini diff --git a/amethyst/src/main/res/values-vi-rVN/strings.xml b/amethyst/src/main/res/values-vi-rVN/strings.xml index d374dcabd9..2392d4a794 100644 --- a/amethyst/src/main/res/values-vi-rVN/strings.xml +++ b/amethyst/src/main/res/values-vi-rVN/strings.xml @@ -1,5 +1,5 @@ - + Hình ảnh nhóm Hành vi trái pháp luật Không xác định diff --git a/amethyst/src/main/res/values-zh-rSG/strings.xml b/amethyst/src/main/res/values-zh-rSG/strings.xml index 66a570486b..3ea04e700d 100644 --- a/amethyst/src/main/res/values-zh-rSG/strings.xml +++ b/amethyst/src/main/res/values-zh-rSG/strings.xml @@ -1,2 +1,2 @@ - + diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip22Comments/GeolocatedComments.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip22Comments/GeolocatedComments.kt index 8346e3b7e4..6f0707638d 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip22Comments/GeolocatedComments.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip22Comments/GeolocatedComments.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId +import com.vitorpamplona.quartz.nip73ExternalIds.location.geohashedScope import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertTrue diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt index 99ac56aa99..5ce927a9df 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent import com.vitorpamplona.quartz.utils.TimeUtils -import kotlin.collections.plus @Immutable class EphemeralChatListEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt index b3ca2fc53c..39c04dc958 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt @@ -36,15 +36,10 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag -import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip19Bech32.addressHints import com.vitorpamplona.quartz.nip19Bech32.addressIds import com.vitorpamplona.quartz.nip19Bech32.eventHints diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt index 83559fd3b1..f8b8a9926f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/TagArray.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.core -import kotlin.collections.indices - typealias TagArray = Array> fun TagArray.builder(initializer: TagArrayBuilder.() -> Unit = {}) = TagArrayBuilder().addAll(this).apply(initializer).build() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt index 82d0264c29..36712c1586 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt @@ -41,7 +41,6 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.PermissionSeri import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorSerializer -import kotlin.jvm.java class JsonMapper { companion object Companion { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt index 1cda88e1d5..cef3107a30 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/PoolEventOutbox.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.pool import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.TimeUtils -import kotlin.collections.any class PoolEventOutbox( val event: Event, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt index d8af62a1b4..25eaf2e19a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -32,9 +32,6 @@ import com.vitorpamplona.quartz.utils.LargeCache import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlin.collections.forEach -import kotlin.collections.isNotEmpty -import kotlin.collections.isNullOrEmpty val UnsupportedRelayCreation: (url: NormalizedRelayUrl) -> IRelayClient = { throw UnsupportedOperationException("Cannot create new relays") diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 9a77aa6074..698b81a0b8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayClient.Companion.DELAY_TO_RECONNECT_IN_MSECS import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index 6af430c1da..5d24e224d2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.nip01Core.relay.normalizer import android.util.Log -import android.util.Log.e import androidx.collection.LruCache import kotlinx.coroutines.CancellationException import org.czeal.rfc3986.URIReference diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt index 181aa88ac7..1a793e857e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.events -import android.R.attr.tag import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt index 968ad43ab0..25811be7b4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt @@ -30,10 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.nip31Alts.AltTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index 402bbb0bcd..d00610eae8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -39,7 +39,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull -import kotlin.collections.plus @Immutable open class BaseThreadedEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt index 061d7a26a1..b102d696f3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -32,16 +32,11 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag -import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.tags.markedETags import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip19Bech32.addressHints import com.vitorpamplona.quartz.nip19Bech32.addressIds import com.vitorpamplona.quartz.nip19Bech32.eventHints diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt index 98870c3745..d5b2a85dca 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/tags/MarkedETag.kt @@ -20,14 +20,12 @@ */ package com.vitorpamplona.quartz.nip10Notes.tags -import android.R.attr.tag import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer.Companion.isRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt index 8dbb539c1a..922952b395 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt @@ -39,7 +39,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils -import kotlin.collections.mapNotNull @Immutable class RepostEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ListEntityExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ListEntityExt.kt index 38ec044925..ee64c350ea 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ListEntityExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/ListEntityExt.kt @@ -33,8 +33,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NSec -import org.apache.commons.lang3.text.translate.CharSequenceTranslator.hex -import kotlin.collections.mapNotNull fun NEvent.toEventHint() = relay.map { EventIdHint(hex, it) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt index 7713994ea9..929a43cb8c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt @@ -33,14 +33,9 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent -import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip19Bech32.addressHints import com.vitorpamplona.quartz.nip19Bech32.addressIds import com.vitorpamplona.quartz.nip19Bech32.eventHints diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt index 4a026c577b..5d064fcbd2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt @@ -38,7 +38,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent -import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip19Bech32.addressHints import com.vitorpamplona.quartz.nip19Bech32.addressIds diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index b6c1e6f956..c9dac836ce 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -20,9 +20,7 @@ */ package com.vitorpamplona.quartz.nip28PublicChat.admin -import android.R.attr.data import android.util.Log -import android.util.Log.e import androidx.compose.runtime.Immutable import com.fasterxml.jackson.core.JsonParseException import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt index e2dbde88b1..384b541da8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt @@ -34,7 +34,6 @@ import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.utils.TimeUtils -import kotlin.collections.map @Immutable class ChannelListEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt index 32fd1e3ced..2c49c8c57a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt @@ -33,17 +33,12 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag -import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.tags.markedETag import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip19Bech32.addressHints import com.vitorpamplona.quartz.nip19Bech32.addressIds import com.vitorpamplona.quartz.nip19Bech32.eventHints diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt index e5f90580b9..ac0f9b9b1e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt index dd87ff02bd..367c85bc88 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt @@ -29,8 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseId import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip31Alts.AltTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt index 894d6701a3..f6483a6730 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/reply/GitReplyEvent.kt @@ -33,7 +33,6 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt index 9ef7531d39..483a49a8d6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip35Torrents/TorrentCommentEvent.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.eTags import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt index e785d00fa8..7e25c3bb78 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt @@ -31,13 +31,10 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt index e1632c8a3a..d21b0239f8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag.Companion.parse import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt index 140a1dcd22..45104fedeb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt @@ -44,7 +44,6 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StreamingTag import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.TotalParticipantsTag import com.vitorpamplona.quartz.utils.TimeUtils -import kotlin.collections.flatten @Immutable class LiveActivitiesEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt index 7bc2b83fa9..197c639f8b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -import kotlin.text.isNullOrBlank class DecryptZapQuery( val loggedInUser: HexKey, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt index 647f42cc23..90dcb76882 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground import android.content.Intent import androidx.collection.LruCache -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor import com.vitorpamplona.quartz.utils.RandomInstance class IntentRequestDatabase { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt index 8eda0d7f24..da4a26369d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt @@ -22,26 +22,6 @@ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground import android.content.Intent -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ interface NewResultProcessor { fun process(intent: Intent) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt index 245d0aa91f..d233b4ee18 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip57Zaps -import android.R.attr.description import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag @@ -31,11 +30,8 @@ import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt index edbe70ff15..efb16f01cf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt @@ -29,11 +29,8 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.utils.TimeUtils @Immutable diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt index 519b13206a..3f94a7c8f5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeAwardEvent.kt @@ -27,12 +27,9 @@ import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt index 84ab8aa2bf..2e47fcbcb4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip58Badges/BadgeProfilesEvent.kt @@ -30,10 +30,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint @Immutable class BadgeProfilesEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt index 362758f780..8b1078e666 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo import com.vitorpamplona.quartz.utils.TimeUtils -import kotlin.collections.map @Immutable class AdvertisedRelayListEvent( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt index 9a7de3008b..bdde75e007 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/tags/AdvertisedRelayInfoTag.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.isLocalHost import com.vitorpamplona.quartz.utils.ensure -import kotlin.text.isBlank class AdvertisedRelayInfo( val relayUrl: NormalizedRelayUrl, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt index 8cb0fb897a..6cba530dd5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/approval/CommunityPostApprovalEvent.kt @@ -31,21 +31,14 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents import com.vitorpamplona.quartz.nip01Core.tags.kinds.kind import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag.Companion.parseAsHint -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag.Companion.parseKey import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt index c18c9b1a2b..11ffee913f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/definition/CommunityDefinitionEvent.kt @@ -30,17 +30,10 @@ import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag.Companion.parseAsHint -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.ParticipantTag.Companion.parseKey import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.DescriptionTag import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ImageTag import com.vitorpamplona.quartz.nip72ModCommunities.definition.tags.ModeratorTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt index 9f95d4dc5f..747bc9f4d8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt @@ -28,15 +28,12 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.tryAndWait -import kotlin.collections.map -import kotlin.collections.plus import kotlin.coroutines.resume @Immutable diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/movies/MovieId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/movies/MovieId.kt index 04babfa41e..580379a3d2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/movies/MovieId.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/movies/MovieId.kt @@ -24,26 +24,6 @@ import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId import com.vitorpamplona.quartz.utils.ensure import kotlin.math.min -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ class MovieId( val isan: String, val hint: String? = null, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/topics/HashtagId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/topics/HashtagId.kt index 166d3a2554..1198100ae3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/topics/HashtagId.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip73ExternalIds/topics/HashtagId.kt @@ -23,26 +23,6 @@ package com.vitorpamplona.quartz.nip73ExternalIds.topics import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId import com.vitorpamplona.quartz.utils.ensure -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ class HashtagId( val topic: String, val hint: String? = null, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt index 7ee4f54400..293ecde409 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip75ZapGoals/GoalEvent.kt @@ -32,12 +32,9 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.references.reference import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt index d1c17157ee..3daf227059 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt @@ -30,20 +30,15 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.addressables.firstTaggedATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.firstTaggedAddress import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag.Companion.parseAsHint import com.vitorpamplona.quartz.nip01Core.tags.people.firstTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.references.ReferenceTag import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent -import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.Companion.parseAsHint import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseAddressAsHint -import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag.Companion.parseEventAsHint import com.vitorpamplona.quartz.nip19Bech32.addressHints import com.vitorpamplona.quartz.nip19Bech32.addressIds import com.vitorpamplona.quartz.nip19Bech32.eventHints diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt index ad1e90b00b..7c22362036 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.nip89AppHandlers.definition import android.util.Log -import android.util.Log.e import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt index e2678d9f0b..a1bd268bc0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt @@ -20,10 +20,8 @@ */ package com.vitorpamplona.quartz.utils -import android.R.attr.value import java.util.concurrent.ConcurrentSkipListMap import java.util.function.BiConsumer -import kotlin.collections.LinkedHashMap class LargeCache { private val cache = ConcurrentSkipListMap() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt index 95fbae954c..98d7db8e91 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/MapOfSetBuilder.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.quartz.utils -import android.R.attr.value - class MapOfSetBuilder { val data = mutableMapOf>() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt index 29458b572c..5063f34277 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/RandomInstance.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.utils -import com.vitorpamplona.quartz.utils.RandomInstance.charPool import java.security.SecureRandom object RandomInstance { From 7bdb3a1a189f666cd1c3a6b79133297481b485f1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Jul 2025 11:50:15 -0400 Subject: [PATCH 096/185] Fixing method name --- .../nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt | 2 +- .../nip55AndroidSigner/api/foreground/NewResultProcessor.kt | 2 +- .../api/foreground/processors/DecryptZapResultProcessor.kt | 2 +- .../api/foreground/processors/DeriveKeyResultProcessor.kt | 2 +- .../api/foreground/processors/LoginResultProcessor.kt | 2 +- .../api/foreground/processors/Nip04DecryptResultProcessor.kt | 2 +- .../api/foreground/processors/Nip04EncryptResultProcessor.kt | 2 +- .../api/foreground/processors/Nip44DecryptResultProcessor.kt | 2 +- .../api/foreground/processors/Nip44EncryptResultProcessor.kt | 2 +- .../api/foreground/processors/SignResultProcessor.kt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt index 90dcb76882..ea02137a9a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt @@ -43,7 +43,7 @@ class IntentRequestDatabase { fun newResponse(data: Intent) { val callId = data.getStringExtra("id") if (callId != null) { - awaitingRequests[callId]?.process(data) + awaitingRequests[callId]?.consume(data) awaitingRequests.remove(callId) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt index da4a26369d..c7281842c7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt @@ -23,5 +23,5 @@ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground import android.content.Intent interface NewResultProcessor { - fun process(intent: Intent) + fun consume(intent: Intent) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt index ff141d1065..8f8cdd68e0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent class DecryptZapResultProcessor( val onReady: (LnZapPrivateEvent) -> Unit, ) : NewResultProcessor { - override fun process(intent: Intent) { + override fun consume(intent: Intent) { val foregroundResult = DecryptZapResponse.parse(intent) if (foregroundResult is SignerResult.Successful) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt index ed17c49f30..26c4368807 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.respon class DeriveKeyResultProcessor( val onReady: (String) -> Unit, ) : NewResultProcessor { - override fun process(intent: Intent) { + override fun consume(intent: Intent) { val foregroundResult = DeriveKeyResponse.parse(intent) if (foregroundResult is SignerResult.Successful) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt index 5d4eda273f..1396e3fd11 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.respon class LoginResultProcessor( val onReady: (HexKey, String) -> Unit, ) : NewResultProcessor { - override fun process(intent: Intent) { + override fun consume(intent: Intent) { val foregroundResult = LoginResponse.parse(intent) if (foregroundResult is SignerResult.Successful) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt index a2c3135233..b38fd62ddb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.respon class Nip04DecryptResultProcessor( val onReady: (String) -> Unit, ) : NewResultProcessor { - override fun process(intent: Intent) { + override fun consume(intent: Intent) { val foregroundResult = Nip04DecryptResponse.parse(intent) if (foregroundResult is SignerResult.Successful) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt index de86a1c072..afcc122580 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.respon class Nip04EncryptResultProcessor( val onReady: (String) -> Unit, ) : NewResultProcessor { - override fun process(intent: Intent) { + override fun consume(intent: Intent) { val foregroundResult = Nip04EncryptResponse.parse(intent) if (foregroundResult is SignerResult.Successful) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt index 45afd04ce3..871ca8b948 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.respon class Nip44DecryptResultProcessor( val onReady: (String) -> Unit, ) : NewResultProcessor { - override fun process(intent: Intent) { + override fun consume(intent: Intent) { val foregroundResult = Nip44DecryptResponse.parse(intent) if (foregroundResult is SignerResult.Successful) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt index 9b6d9e6783..eecb92155f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.respon class Nip44EncryptResultProcessor( val onReady: (String) -> Unit, ) : NewResultProcessor { - override fun process(intent: Intent) { + override fun consume(intent: Intent) { val foregroundResult = Nip44EncryptResponse.parse(intent) if (foregroundResult is SignerResult.Successful) { onReady(foregroundResult.result.ciphertext) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt index 7fd670a875..dc90d7b7df 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt @@ -31,7 +31,7 @@ class SignResultProcessor( val unsignedEvent: Event, val onReady: (Event) -> Unit, ) : NewResultProcessor { - override fun process(intent: Intent) { + override fun consume(intent: Intent) { val foregroundResult = SignResponse.parse(intent, unsignedEvent) if (foregroundResult is SignerResult.Successful) { From 58e353fb4070f959fb9f47290f1744cb4fba26bd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 18 Jul 2025 17:17:27 -0400 Subject: [PATCH 097/185] Moves NIP-55 calls to be suspending functions. Moves NIP-55 calls to include an ID per call, not per event. Adds error handling facilities to the Signer functions. Moves the indexing of the decrypted objects to outside the LocalCache Migrates Signers to become suspending functions. Migrates Decryption caching systems to outside the Events themselves. Migrates all NIP-51 lists to the new structure. Migrates Drafts and NIP-04 and NIP-17 DMs to the new structure Migrates Bookmarks to the new structure. Changes the Room route to avoid using hashcode. --- .../amethyst/ImageUploadTesting.kt | 35 +- .../vitorpamplona/amethyst/OkHttpOtsTest.kt | 23 +- .../ThreadDualAxisChartAssemblerTest.kt | 184 +-- .../com/vitorpamplona/amethyst/Amethyst.kt | 4 +- .../amethyst/LocalPreferences.kt | 19 +- .../vitorpamplona/amethyst/model/Account.kt | 1116 +++++++---------- .../amethyst/model/AccountSettings.kt | 24 +- .../amethyst/model/AccountSyncedSettings.kt | 11 +- .../vitorpamplona/amethyst/model/Channel.kt | 6 +- .../amethyst/model/LocalCache.kt | 492 ++------ .../com/vitorpamplona/amethyst/model/Note.kt | 105 +- .../com/vitorpamplona/amethyst/model/User.kt | 92 -- .../PrivateStorageRelayListDecryptionCache.kt | 29 + .../edits/PrivateStorageRelayListState.kt | 29 +- .../EphemeralChatListDecryptionCache.kt | 33 +- .../model/emphChat/EphemeralChatListState.kt | 54 +- .../nip01UserMetadata/UserMetadataState.kt | 7 +- .../FollowListOutboxRelays.kt | 2 +- .../model/nip02FollowLists/FollowListState.kt | 22 +- .../nip02FollowLists/FollowsPerOutboxRelay.kt | 2 +- .../model/nip17Dms/DmRelayListState.kt | 13 +- .../model/nip18Reposts/RepostAction.kt | 12 +- .../model/nip25Reactions/ReactionAction.kt | 44 +- .../PublicChatListDecryptionCache.kt | 33 +- .../nip28PublicChats/PublicChatListState.kt | 74 +- .../model/nip30CustomEmojis/EmojiPackState.kt | 30 +- .../nip38UserStatuses/UserStatusAction.kt | 35 +- .../nip47WalletConnect/NwcSignerState.kt | 181 +++ .../model/nip51Lists/BookmarkListState.kt | 276 ++++ .../model/nip51Lists/HiddenUsersState.kt | 16 +- .../BlockPeopleListState.kt | 96 +- .../PeopleListDecryptionCache.kt | 56 + .../BlockedRelayListDecryptionCache.kt | 29 + .../BlockedRelayListState.kt | 29 +- .../geohashLists/GeohashListCard.kt | 10 +- .../GeohashListDecryptionCache.kt | 76 ++ .../{ => geohashLists}/GeohashListState.kt | 66 +- .../HashtagListDecryptionCache.kt | 36 + .../{ => hashtagLists}/HashtagListState.kt | 66 +- .../muteList/MuteListDecryptionCache.kt | 56 + .../{ => muteList}/MuteListState.kt | 112 +- .../relayLists/GenericRelayListCache.kt | 76 ++ .../nip51Lists/relayLists/RelayListCard.kt | 31 + .../SearchRelayListDecryptionCache.kt | 29 + .../searchRelays}/SearchRelayListState.kt | 25 +- .../TrustedRelayListDecryptionCache.kt | 29 + .../TrustedRelayListState.kt | 31 +- .../model/nip56Reports/ReportAction.kt | 22 +- .../nip65RelayList/Nip65RelayListState.kt | 11 +- .../CommunityListDecryptionCache.kt | 46 + .../nip72Communities/CommunityListState.kt | 92 +- .../nip78AppSpecific/AppSpecificState.kt | 51 +- .../FileStorageServerListState.kt | 8 +- .../nipB7Blossom/BlossomServerListState.kt | 33 +- .../model/{ => privateChats}/Chatroom.kt | 26 +- .../model/privateChats/ChatroomList.kt | 96 ++ .../serverList/MergedFollowListsState.kt | 11 +- .../MergedFollowPlusMineRelayListsState.kt | 2 +- .../serverList/TrustedRelayListsState.kt | 4 +- .../model/topNavFeeds/FeedDecryptionCaches.kt | 35 + .../topNavFeeds/FeedTopNavFilterState.kt | 3 +- .../topNavFeeds/noteBased/NoteFeedFlow.kt | 65 +- .../amethyst/service/ZapPaymentHandler.kt | 46 +- .../amethyst/service/cashu/v3/V3Parser.kt | 2 +- .../amethyst/service/cashu/v4/V4Parser.kt | 1 - .../service/location/LocationState.kt | 3 +- .../service/location/ReverseGeolocation.kt | 1 + .../EventNotificationConsumer.kt | 170 ++- .../service/notifications/RegisterAccounts.kt | 10 +- .../authCommand/model/AuthCoordinator.kt | 4 +- .../FilterAccountInfoAndListsFromKey.kt | 6 +- .../FilterBasicAccountInfoFromKeys.kt | 4 +- .../metadata/FilterDraftsAndReportsFromKey.kt | 2 +- .../metadata/FilterFollowsAndMutesFromKey.kt | 6 +- .../reqCommand/user/UserObservers.kt | 65 +- .../subassemblies/SearchPostsByText.kt | 6 +- .../relayClient/speedLogger/FrameStat.kt | 77 ++ .../KindGroup.kt} | 98 +- .../speedLogger/RelaySpeedLogger.kt | 66 + .../service/uploads/MultiOrchestrator.kt | 71 +- .../service/uploads/UploadOrchestrator.kt | 5 + .../uploads/nip96/ServerInfoRetriever.kt | 2 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 8 +- .../amethyst/ui/actions/EditPostViewModel.kt | 46 +- .../amethyst/ui/actions/MediaSaverToDisk.kt | 2 +- .../amethyst/ui/actions/NewMediaModel.kt | 66 +- .../ui/actions/NewUserMetadataViewModel.kt | 4 + .../amethyst/ui/components/ClickableRoute.kt | 2 +- .../amethyst/ui/components/RichTextViewer.kt | 2 +- .../amethyst/ui/dal/FilterByListParams.kt | 4 +- .../amethyst/ui/navigation/AppNavigation.kt | 2 +- .../ui/navigation/routes/RouteMaker.kt | 39 +- .../amethyst/ui/navigation/routes/Routes.kt | 14 +- .../navigation/topbars/FeedFilterSpinner.kt | 6 +- .../amethyst/ui/note/BadgeCompose.kt | 2 +- .../amethyst/ui/note/MessageSetCompose.kt | 2 +- .../amethyst/ui/note/MultiSetCompose.kt | 2 +- .../ui/note/NIP05VerificationDisplay.kt | 4 +- .../amethyst/ui/note/NoteCompose.kt | 12 +- .../amethyst/ui/note/NoteQuickActionMenu.kt | 2 +- .../amethyst/ui/note/PollNote.kt | 3 +- .../amethyst/ui/note/PollNoteViewModel.kt | 19 +- .../amethyst/ui/note/ReactionsRow.kt | 16 +- .../ui/note/UpdateReactionTypeDialog.kt | 36 +- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 138 +- .../ui/note/UpdateZapAmountViewModel.kt | 146 +++ .../amethyst/ui/note/elements/DropDownMenu.kt | 35 +- .../amethyst/ui/note/elements/ForkInfo.kt | 4 +- .../ui/note/elements/ZapTheDevsCard.kt | 2 +- .../nip22Comments/CommentPostViewModel.kt | 57 +- .../amethyst/ui/note/types/ChatMessage.kt | 2 +- .../ui/note/types/ChatMessageEncryptedFile.kt | 2 +- .../amethyst/ui/note/types/CommunityHeader.kt | 11 +- .../amethyst/ui/note/types/FollowList.kt | 4 +- .../amethyst/ui/note/types/Highlight.kt | 2 +- .../amethyst/ui/note/types/PeopleList.kt | 2 +- .../amethyst/ui/note/types/PrivateMessage.kt | 2 +- .../amethyst/ui/note/types/RelayList.kt | 66 +- .../ui/note/types/TextModification.kt | 2 +- .../amethyst/ui/note/types/TorrentComment.kt | 2 +- .../ui/screen/AccountStateViewModel.kt | 2 +- .../amethyst/ui/screen/FollowListState.kt | 8 +- .../ui/screen/loggedIn/AccountViewModel.kt | 543 ++++---- .../loggedIn/DecryptAndIndexProcessor.kt | 382 ++++++ .../ui/screen/loggedIn/LoggedInPage.kt | 7 +- .../loggedIn/PrecacheNewNotesProcessor.kt | 162 --- .../loggedIn/bookmarks/BookmarkListScreen.kt | 5 +- .../dal/BookmarkPrivateFeedFilter.kt | 25 +- .../bookmarks/dal/BookmarkPublicFeedFilter.kt | 22 +- .../chats/privateDM/ChatroomScreen.kt | 73 +- .../chats/privateDM/dal/ChatroomFeedFilter.kt | 4 +- .../header/NewChatroomSubjectDialog.kt | 7 +- .../privateDM/header/RoomNameOnlyDisplay.kt | 13 +- .../privateDM/send/ChatNewMessageViewModel.kt | 120 +- .../chats/privateDM/send/NewGroupDMScreen.kt | 6 +- .../privateDM/send/upload/ChatFileSender.kt | 10 +- .../privateDM/send/upload/ChatFileUploader.kt | 125 +- .../metadata/ChannelMetadataScreen.kt | 27 +- .../metadata/ChannelMetadataViewModel.kt | 38 +- .../send/ChannelNewMessageViewModel.kt | 42 +- .../chats/rooms/ChatroomHeaderCompose.kt | 2 +- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 32 +- .../rooms/dal/ChatroomListNewFeedFilter.kt | 36 +- .../chats/rooms/twopane/MessagesTwoPane.kt | 9 +- .../loggedIn/discover/ChannelCardCompose.kt | 2 +- .../loggedIn/discover/DiscoverScreen.kt | 2 +- .../DiscoverLongFormFeedFilter.kt | 4 +- .../nip28Chats/DiscoverChatFeedFilter.kt | 6 +- .../DiscoverFollowSetsFeedFilter.kt | 6 +- .../discover/nip51FollowSets/FollowSetCard.kt | 6 +- .../FilterFollowSetsByAllCommunities.kt | 2 +- .../FilterFollowSetsByAuthors.kt | 2 +- .../FilterFollowSetsByCommunity.kt | 2 +- .../FilterFollowSetsByGeohash.kt | 2 +- .../FilterFollowSetsByHashtag.kt | 2 +- .../subassemblies/FilterFollowSetsGlobal.kt | 2 +- .../DiscoverLiveFeedFilter.kt | 4 +- .../DiscoverCommunityFeedFilter.kt | 4 +- .../nip90DVMs/DiscoverNIP89FeedFilter.kt | 4 +- .../DiscoverMarketplaceFeedFilter.kt | 4 +- .../nip99Classifieds/NewProductScreen.kt | 28 +- .../nip99Classifieds/NewProductViewModel.kt | 13 +- .../dvms/DvmContentDiscoveryScreen.kt | 12 +- .../NIP90ContentDiscoveryResponseFilter.kt | 4 +- .../loggedIn/home/ShortNotePostViewModel.kt | 52 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 4 +- .../dal/NotificationFeedFilter.kt | 6 +- .../dal/UserProfileBookmarksFeedFilter.kt | 34 +- .../datasource/FilterUserProfileLists.kt | 8 +- .../profile/gallery/GalleryCardCompose.kt | 2 +- .../dal/UserProfileGalleryFeedFilter.kt | 4 +- .../profile/header/DisplayLNAddress.kt | 4 +- .../loggedIn/profile/header/MessageButton.kt | 16 +- .../loggedIn/redirect/LoadRedirectScreen.kt | 2 +- .../loggedIn/relays/AllRelayListScreen.kt | 18 +- .../blocked/BlockedRelayListViewModel.kt | 6 +- .../relays/common/BasicRelaySetupInfoModel.kt | 11 +- .../connected/ConnectedRelayListViewModel.kt | 2 +- .../relays/dm/AddDMRelayListDialog.kt | 4 +- .../relays/dm/DMRelayListViewModel.kt | 2 +- .../relays/local/LocalRelayListViewModel.kt | 2 +- .../nip37/PrivateOutboxRelayListViewModel.kt | 6 +- .../relays/nip65/Nip65RelayListViewModel.kt | 9 +- .../relays/search/SearchRelayListViewModel.kt | 6 +- .../trusted/TrustedRelayListViewModel.kt | 6 +- .../search/AddInboxRelayForSearchCard.kt | 51 +- .../search/AddSearchRelayListDialog.kt | 4 +- .../loggedIn/settings/NIP47SetupScreen.kt | 8 +- .../settings/SecurityFiltersScreen.kt | 2 +- .../loggedIn/settings/UserSettingsScreen.kt | 9 +- .../loggedIn/threadview/ThreadFeedView.kt | 10 +- .../ui/screen/loggedIn/video/VideoScreen.kt | 2 +- .../loggedIn/video/dal/VideoFeedFilter.kt | 4 +- .../loggedOff/login/ExternalSignerButton.kt | 2 +- amethyst/src/main/res/values/strings.xml | 7 + .../components/TranslatableRichTextViewer.kt | 14 +- .../quartz/benchmark/GiftWrapBenchmark.kt | 103 +- .../amethyst/commons/preview/BlurhashTest.kt | 2 +- .../commons/compose/AsyncCachedState.kt | 37 +- .../quartz/nip03Timestamp/ots/OtsTest.kt | 20 +- .../quartz/nip46RemoteSigner/Nip46Test.kt | 299 +++-- .../quartz/nip57Zaps/PrivateZapTests.kt | 124 +- .../nip59GiftWraps/GiftWrapEventTest.kt | 694 +++++----- .../com/vitorpamplona/quartz/EventFactory.kt | 52 +- .../decoupling/DecoupledCipher.kt | 58 +- .../decoupling/EncryptionKeyCache.kt | 18 +- .../edits/PrivateOutboxRelayListEvent.kt | 173 --- .../edits/TextNoteModificationEvent.kt | 7 +- .../experimental/ephemChat/chat/RoomId.kt | 3 + .../ephemChat/list/EphemeralChatListEvent.kt | 158 ++- .../ephemChat/list/TagArrayBuilderExt.kt | 3 + .../ephemChat/list/TagArrayExt.kt | 28 + .../profileGallery/GalleryListEvent.kt | 59 +- .../RelationshipStatusEvent.kt | 18 +- .../relationshipStatus/TagArrayBuilderExt.kt | 4 +- .../tags/{PetnameTag.kt => PetNameTag.kt} | 4 +- .../quartz/nip01Core/EventExt.kt | 11 +- .../quartz/nip01Core/crypto/EventAssembler.kt | 2 +- .../quartz/nip01Core/jackson/JsonMapper.kt | 2 +- .../client/accessories/RelayAuthenticator.kt | 9 +- .../client/single/basic/BasicRelayClient.kt | 2 - .../quartz/nip01Core/signers/NostrSigner.kt | 97 +- .../nip01Core/signers/NostrSignerInternal.kt | 84 +- .../nip01Core/signers/NostrSignerSync.kt | 65 +- .../nip01Core/signers/SignerExceptions.kt | 62 + .../nip01Core/signers/caches/CacheResults.kt | 37 + .../nip01Core/signers/caches/DecryptCache.kt | 121 ++ .../nip01Core/tags/addressables/ATag.kt | 2 +- .../nip01Core/tags/addressables/Address.kt | 4 +- .../nip02FollowList/ContactListEvent.kt | 39 +- .../quartz/nip04Dm/PrivateDMCache.kt | 55 + .../quartz/nip04Dm/messages/PrivateDmEvent.kt | 67 +- .../nip11RelayInfo/Nip11RelayInformation.kt | 4 +- .../quartz/nip17Dm/NIP17Factory.kt | 143 +-- .../quartz/nip17Dm/base/BaseDMGroupEvent.kt | 3 + .../quartz/nip17Dm/base/ChatroomKey.kt | 4 +- .../quartz/nip17Dm/base/NIP17Group.kt | 2 + .../settings/ChatMessageRelayListEvent.kt | 30 +- .../quartz/nip18Reposts/GenericRepostEvent.kt | 7 +- .../quartz/nip19Bech32/entities/NAddress.kt | 1 - .../quartz/nip19Bech32/entities/NNote.kt | 2 +- .../nip22Comments/tags/ReplyIdentifierTag.kt | 4 +- .../nip28PublicChat/list/ChannelListEvent.kt | 282 ++--- .../list/TagArrayBuilderExt.kt | 3 + .../nip28PublicChat/list/TagArrayExt.kt | 28 + .../nip28PublicChat/list/tags/ChannelTag.kt | 119 ++ .../nip30CustomEmoji/pack/EmojiPackEvent.kt | 4 +- .../vitorpamplona/quartz/nip31Alts/AltTag.kt | 3 + .../quartz/nip34Git/patch/GitPatchEvent.kt | 7 +- .../quartz/nip37Drafts/DraftBuilder.kt | 29 +- .../quartz/nip37Drafts/DraftEvent.kt | 155 +-- .../quartz/nip37Drafts/DraftEventCache.kt | 60 + .../PrivateOutboxRelayListEvent.kt | 121 ++ .../quartz/nip38UserStatus/StatusEvent.kt | 21 +- .../quartz/nip42RelayAuth/RelayAuthEvent.kt | 14 +- .../quartz/nip44Encryption/Nip44.kt | 34 +- .../quartz/nip44Encryption/Nip44v1.kt | 27 +- .../quartz/nip44Encryption/Nip44v2.kt | 20 +- .../nip46RemoteSigner/NostrConnectEvent.kt | 69 +- .../LnZapPaymentRequestEvent.kt | 53 +- .../LnZapPaymentResponseEvent.kt | 43 +- .../nip47WalletConnect/Nip47WalletConnect.kt | 2 +- .../NostrWalletConnectRequestCache.kt | 52 + .../NostrWalletConnectResponseCache.kt | 52 + .../nip50Search/SearchRelayListEvent.kt | 97 +- .../nip51Lists/BlockedRelayListEvent.kt | 105 -- .../quartz/nip51Lists/BookmarkListEvent.kt | 237 ---- .../quartz/nip51Lists/GeneralListEvent.kt | 188 --- .../quartz/nip51Lists/MuteListEvent.kt | 285 ----- .../quartz/nip51Lists/PeopleListEvent.kt | 305 ----- .../quartz/nip51Lists/PinListEvent.kt | 7 +- .../nip51Lists/PrivateTagArrayBuilder.kt | 156 +-- .../quartz/nip51Lists/PrivateTagArrayEvent.kt | 65 +- .../nip51Lists/PrivateTagArrayEventCache.kt | 61 + .../quartz/nip51Lists/RelaySetEvent.kt | 62 - .../quartz/nip51Lists/TagArrayExt.kt | 9 + .../nip51Lists/TrustedRelayListEvent.kt | 105 -- .../bookmarkList/BookmarkListEvent.kt | 212 ++++ .../bookmarkList/TagArrayBuilderExt.kt | 29 + .../bookmarkList/tags/AddressBookmark.kt | 150 +++ .../bookmarkList/tags/BookmarkIdTag.kt | 35 + .../bookmarkList/tags/EventBookmark.kt | 119 ++ .../encryption/NostrSignerExt.kt} | 42 +- .../encryption/PrivateTagsInContent.kt | 72 +- .../{ => followList}/FollowListEvent.kt | 124 +- .../followList/TagArrayBuilderExt.kt | 29 + .../nip51Lists/followList/TagArrayExt.kt | 30 + .../geohashList/GeohashListEvent.kt | 220 ++++ .../TagArrayBuilderExt.kt | 4 +- .../nip51Lists/geohashList/TagArrayExt.kt | 28 + .../hashtagList/HashtagListEvent.kt | 217 ++++ .../TagArrayBuilderExt.kt | 5 +- .../nip51Lists/hashtagList/TagArrayExt.kt | 28 + .../nip51Lists/interests/HashtagListEvent.kt | 220 ---- .../nip51Lists/locations/GeohashListEvent.kt | 220 ---- .../nip51Lists/muteList/MuteListEvent.kt | 191 +++ .../nip51Lists/muteList/TagArrayBuilderExt.kt | 26 + .../quartz/nip51Lists/muteList/TagArrayExt.kt | 38 + .../nip51Lists/muteList/tags/MuteTag.kt | 35 + .../nip51Lists/muteList/tags/UserTag.kt | 103 ++ .../muteList/tags/WordTag.kt} | 46 +- .../nip51Lists/peopleList/PeopleListEvent.kt | 218 ++++ .../peopleList/TagArrayBuilderExt.kt | 29 + .../nip51Lists/peopleList/TagArrayExt.kt | 38 + .../relayLists/BlockedRelayListEvent.kt | 120 ++ .../relayLists/TrustedRelayListEvent.kt | 121 ++ .../relayLists}/tags/RelayTag.kt | 2 +- .../relayLists/tags/TagArrayBuilderExt.kt | 44 + .../nip51Lists/relayLists/tags/TagArrayExt.kt | 27 + .../nip51Lists/relaySets/RelaySetEvent.kt | 116 ++ .../quartz/nip51Lists/tags/TitleTag.kt | 1 - .../nip52Calendar/CalendarDateSlotEvent.kt | 7 +- .../quartz/nip52Calendar/CalendarEvent.kt | 7 +- .../quartz/nip52Calendar/CalendarRSVPEvent.kt | 7 +- .../nip52Calendar/CalendarTimeSlotEvent.kt | 7 +- .../streaming/LiveActivitiesEvent.kt | 7 +- .../quartz/nip54Wiki/WikiNoteEvent.kt | 7 +- .../nip55AndroidSigner/api/CommandType.kt | 2 +- .../nip55AndroidSigner/api/SignerResult.kt | 55 +- .../api/background/queries/DecryptZapQuery.kt | 10 +- .../api/background/queries/DeriveKeyQuery.kt | 4 +- .../api/background/queries/LoginQuery.kt | 4 +- .../background/queries/Nip04DecryptQuery.kt | 4 +- .../background/queries/Nip04EncryptQuery.kt | 4 +- .../background/queries/Nip44DecryptQuery.kt | 4 +- .../background/queries/Nip44EncryptQuery.kt | 4 +- .../api/background/queries/SignQuery.kt | 16 +- .../background/utils/ContentResolverExt.kt | 6 +- .../api/foreground/IntentRequestDatabase.kt | 64 - .../api/foreground/IntentRequestManager.kt | 138 ++ .../intents/responses/DecryptZapResponse.kt | 10 +- .../intents/responses/DeriveKeyResponse.kt | 6 +- .../intents/responses/LoginResponse.kt | 6 +- .../intents/responses/Nip04DecryptResponse.kt | 6 +- .../intents/responses/Nip04EncryptResponse.kt | 6 +- .../intents/responses/Nip44DecryptResponse.kt | 6 +- .../intents/responses/Nip44EncryptResponse.kt | 6 +- .../intents/responses/SignResponse.kt | 18 +- .../processors/DeriveKeyResultProcessor.kt | 39 - .../processors/Nip04DecryptResultProcessor.kt | 39 - .../processors/Nip04EncryptResultProcessor.kt | 39 - .../processors/Nip44DecryptResultProcessor.kt | 39 - .../processors/Nip44EncryptResultProcessor.kt | 38 - .../client/IActivityLauncher.kt | 2 + .../client/NostrSignerExternal.kt | 129 +- .../handlers/BackgroundRequestHandler.kt | 95 +- .../handlers/ForegroundRequestHandler.kt | 92 +- .../quartz/nip56Reports/ReportEvent.kt | 24 - .../quartz/nip57Zaps/LnZapPrivateEvent.kt | 9 +- .../quartz/nip57Zaps/LnZapRequestEvent.kt | 102 +- .../quartz/nip57Zaps/PrivateZapCache.kt | 58 + .../nip57Zaps/PrivateZapRequestBuilder.kt | 66 +- .../nip59Giftwrap/rumors/RumorAssembler.kt | 58 + .../nip59Giftwrap/seals/SealedRumorEvent.kt | 64 +- .../nip59Giftwrap/wraps/GiftWrapEvent.kt | 56 +- .../AdvertisedRelayListEvent.kt | 25 +- .../follow/CommunityListEvent.kt | 351 +++--- .../follow/TagArrayBuilderExt.kt | 6 + .../nip72ModCommunities/follow/TagArrayExt.kt | 30 + .../follow/tags/CommunityTag.kt | 150 +++ .../nip78AppData/AppSpecificDataEvent.kt | 7 +- .../quartz/nip84Highlights/HighlightEvent.kt | 7 +- .../definition/AppDefinitionEvent.kt | 5 +- .../NIP90ContentDiscoveryRequestEvent.kt | 7 +- .../NIP90ContentDiscoveryResponseEvent.kt | 8 +- .../quartz/nip90Dvms/NIP90StatusEvent.kt | 8 +- .../NIP90UserDiscoveryRequestEvent.kt | 8 +- .../NIP90UserDiscoveryResponseEvent.kt | 8 +- .../nipB7Blossom/BlossomAuthorizationEvent.kt | 27 +- .../nipB7Blossom/BlossomServersEvent.kt | 21 +- .../vitorpamplona/quartz/utils/ArrayUtils.kt | 2 + .../quartz/utils/LibSodiumInstance.kt | 4 +- .../quartz/utils/ParallelUtils.kt | 34 +- .../vitorpamplona/quartz/utils/TimeUtils.kt | 3 + 374 files changed, 9807 insertions(+), 8720 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt => amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt (53%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt => amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt (53%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{ => blockPeopleList}/BlockPeopleListState.kt (58%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/PeopleListDecryptionCache.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{ => blockedRelays}/BlockedRelayListState.kt (80%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt => amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt (86%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{ => geohashLists}/GeohashListState.kt (67%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{ => hashtagLists}/HashtagListState.kt (67%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{ => muteList}/MuteListState.kt (67%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/{nip50Search => nip51Lists/searchRelays}/SearchRelayListState.kt (84%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/{ => trustedRelays}/TrustedRelayListState.kt (78%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/{ => privateChats}/Chatroom.kt (79%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/FrameStat.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/{RelaySpeedLogger.kt => speedLogger/KindGroup.kt} (52%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt delete mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/PrecacheNewNotesProcessor.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/TagArrayExt.kt rename quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/{PetnameTag.kt => PetNameTag.kt} (95%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/SignerExceptions.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/CacheResults.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/DecryptCache.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEventCache.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/privateOutbox/PrivateOutboxRelayListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/NostrWalletConnectRequestCache.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/NostrWalletConnectResponseCache.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BlockedRelayListEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEventCache.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TrustedRelayListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/BookmarkIdTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt => nip51Lists/encryption/NostrSignerExt.kt} (59%) rename quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/{ => followList}/FollowListEvent.kt (55%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/GeohashListEvent.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/{locations => geohashList}/TagArrayBuilderExt.kt (88%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/HashtagListEvent.kt rename quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/{interests => hashtagList}/TagArrayBuilderExt.kt (84%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayExt.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/MuteListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/MuteTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{nip50Search/tags/RelayTag.kt => nip51Lists/muteList/tags/WordTag.kt} (54%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BlockedRelayListEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/TrustedRelayListEvent.kt rename quartz/src/main/java/com/vitorpamplona/quartz/{experimental/edits => nip51Lists/relayLists}/tags/RelayTag.kt (97%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relaySets/RelaySetEvent.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt delete mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/RumorAssembler.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index acd0939e90..ef98f7fa90 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -24,8 +24,9 @@ import android.graphics.Bitmap import android.graphics.Color import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState import com.vitorpamplona.amethyst.service.okhttp.DefaultContentTypeInterceptor import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.service.uploads.ImageDownloader @@ -38,6 +39,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.utils.sha256.sha256 import junit.framework.TestCase.assertEquals import junit.framework.TestCase.fail @@ -49,6 +51,7 @@ import okhttp3.OkHttpClient import org.junit.Assert import org.junit.Ignore import org.junit.Test +import org.junit.runner.Request.method import org.junit.runner.RunWith import java.io.ByteArrayOutputStream import kotlin.random.Random @@ -57,12 +60,16 @@ import kotlin.random.Random class ImageUploadTesting { companion object { val accountSettings = AccountSettings(KeyPair()) + val signer = NostrSignerInternal(accountSettings.keyPair) + val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val cache = LocalCache - val account = - Account( + val blossomServerListState = + BlossomServerListState( + signer = signer, + cache = cache, + scope = scope, settings = accountSettings, - signer = NostrSignerInternal(accountSettings.keyPair), - scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), ) } @@ -111,7 +118,7 @@ class ImageUploadTesting { sensitiveContent = null, serverBaseUrl = server.baseUrl, okHttpClient = { client }, - httpAuth = account::createBlossomUploadAuth, + httpAuth = blossomServerListState::createBlossomUploadAuth, context = InstrumentationRegistry.getInstrumentation().targetContext, ) @@ -139,25 +146,27 @@ class ImageUploadTesting { { client }, ) - val paylod = getBitmap() - val inputStream = paylod.inputStream() + val payload = getBitmap() + val inputStream = payload.inputStream() val result = Nip96Uploader() .upload( inputStream = inputStream, - length = paylod.size.toLong(), + length = payload.size.toLong(), contentType = "image/png", alt = null, sensitiveContent = null, server = serverInfo, okHttpClient = { client }, onProgress = {}, - httpAuth = account::createHTTPAuthorization, + httpAuth = { url, method, body -> + signer.sign(HTTPAuthorizationEvent.build(url, method, body)) + }, context = InstrumentationRegistry.getInstrumentation().targetContext, ) val url = result.url!! - val size = result.size + val size = result.size?.toInt() val dim = result.dimension val hash = result.sha256 @@ -183,7 +192,7 @@ class ImageUploadTesting { assertEquals("${server.name}: Invalid dimensions", it.dim.toString(), dim.toString()) } if (size != null) { - assertEquals("${server.name}: Invalid size", it.size.toString(), size) + assertEquals("${server.name}: Invalid size", it.size, size) } }, onFailure = { fail("${server.name}: It should not fail") }, @@ -225,6 +234,7 @@ class ImageUploadTesting { testBase(ServerName("sove", "https://sove.rent", ServerType.NIP96)) } + @Ignore("Not Working anymore") @Test() fun testNostrBuild() = runBlocking { @@ -239,6 +249,7 @@ class ImageUploadTesting { } @Test() + @Ignore("Not Working anymore") fun testVoidCat() = runBlocking { testBase(ServerName("void.cat", "https://void.cat", ServerType.NIP96)) diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt index a2178a5061..d039911477 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/OkHttpOtsTest.kt @@ -30,12 +30,10 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking import okhttp3.OkHttpClient -import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class OkHttpOtsTest { @@ -49,9 +47,9 @@ class OkHttpOtsTest { val resolver = OtsResolver( OkHttpBitcoinExplorer( - OkHttpBitcoinExplorer.MEMPOOL_API_URL, + baseAPI = OkHttpBitcoinExplorer.MEMPOOL_API_URL, client = OkHttpClient.Builder().build(), - otsCache, + cache = otsCache, ), OkHttpCalendarBuilder { OkHttpClient.Builder().build() }, ) @@ -80,20 +78,15 @@ class OkHttpOtsTest { @Test fun createOTSEventAndVerify() { val signer = NostrSignerInternal(KeyPair()) - var ots: OtsEvent? = null - - val countDownLatch = CountDownLatch(1) val otsFile = OtsEvent.stamp(otsEvent2Digest, resolver) - signer.sign(OtsEvent.build(otsEvent2Digest, otsFile)) { - ots = it - countDownLatch.countDown() - } + val ots = + runBlocking { + signer.sign(OtsEvent.build(otsEvent2Digest, otsFile)) + } - Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - - println(ots!!.toJson()) + println(ots.toJson()) println(resolver.info(ots.otsByteArray())) // Should not be valid because we need to wait for confirmations diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt index 8a028d15e0..f1d881ce5b 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt @@ -25,10 +25,13 @@ import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.location.LocationState +import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.verify @@ -37,86 +40,110 @@ import junit.framework.TestCase.assertEquals import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ThreadDualAxisChartAssemblerTest { - val db = - """ - [ - {"id":"741f5367a9415f4d6f19c0f57a1e4647c8ed8309b53b0da2d82fc4ebfba03b2c","pubkey":"d85c99afd244911e0aaf800cbea4221df557f06f8a4ff2cbe84b24e0b9e728fc","created_at":1684674845,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Getting my head around this","sig":"069874040bac26a219777fc0f90b8f4df71e38c30e3e6a953d53222499d8e0c5a8f32c6b4204d14eb335bb654f01c5610372d9dc00062284b8e0f2bb98c7ed85"}, - {"id":"22323fc72b4c37f93ea21f6069684339ce5f63111161c81f2aa3de4a21bfe83b","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1683571810,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test","sig":"f6d3bcf0f8e07d06720a2527f95910ee5a66aa889dd9261514921a155bbc3e9a3c7d721f5a2ec948cd795183f85dbe80f2ee2e36663f9d1de33fc03274ccb9d9"}, - {"id":"14525fcaae530a029f782fd361dd0cd66634c3e23020bd19e66fe11c1e254e32","pubkey":"afd563434a737334d69db899e4a32fe38d73a182bb6d1e91d83a2c4c4e04737c","created_at":1682968749,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is most amazing thing yet for nostr!","sig":"3dcbb058b8fa6e6f388f5b1a26d21b888dbc91bb831b0ed80ee30d9d2494fbbbb40dac9daf637b948c3afe1f32c2c662eca8d7acc0ba31f23b89edc8ade99631"}, - {"id":"98ae0d6d10e494ed0bf70feb577e8225c6a6732c7af3d29f88bfe1b87d4439e6","pubkey":"83fd07de9b763334cc9d46f2785c2558e6c2eabfe7d0c6ec214667cbaec50d47","created_at":1681995243,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"true if big","sig":"bb7d97e81207096d8615ebb0458c3023fa1b8f3aac463f3f0293ae94e2b90d043ad371067ce984a2ab2ba49cd7432a553f128b8d2d678238551538ad657bd75c"}, - {"id":"36e262e71b7e8bcae946f69885b8c3614e318e82864437342cf50e8b9ab7229d","pubkey":"b111d517452f9ef015e16d60ae623a6b66af63024eec941b0653bfee0dd667d4","created_at":1681979196,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"this is wonderful. great work.","sig":"205a46d867c22cbb09c7dcc4351330a95304c0d80540c85b9ea39f7f464ea60df1ef5006c1f93b862536f04d8a7855e4cc2a915d7a01437cd30f7f12c716b239"}, - {"id":"37725c2924ca267d66c2c27c2dae65550c07e7b883034cf1ea69671883430642","pubkey":"90b9bec74789688e515125596ab6350bfe646176ac75742275063922c5fea010","created_at":1681944950,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Epic!","sig":"c461b97a3cdc5cf967e6f32c59c94b1b912c296feb1cbb7943c81731e558b10a3674982d6990567260d9fb033f5a3d3db8984b1eb362715d68ba46034a8b1b86"}, - {"id":"8ba54cfb6375270e8ae97a7e0992c1a0dbaa4cd46af8309d67a839e86789fde6","pubkey":"c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11","created_at":1681944377,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is incredible... Bravo, Pablo, and shoutout to Gigi.","sig":"fb1a84d4859742b001b810c98a5e9301bc861fa2d65a711cb21c57f6e62c98f27e5b0c0c6432ef1111bc6cc52aa8cb0ae909d28af50c497d125950bfaeade475"}, - {"id":"53410bc6d47e87f3f18ecbc93c716b5a6ef8ee3805516b2ff4d155154a685b7c","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681945005,"kind":1,"tags":[["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["e","8ba54cfb6375270e8ae97a7e0992c1a0dbaa4cd46af8309d67a839e86789fde6"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11"]],"content":"Thank you, my friend 🤙 ","sig":"3d2fbcefdfac2183601076c8f9e518e05620cefe74cba233684cce931a69b51befa329c94734e434805bd9382ff3dad36e42d1955d4e47c822e973ba69b7377f"}, - {"id":"e383476cb1ce5accde11d4b1338424fa32c3724cf96e6214af8e5e852981728a","pubkey":"50de492cfe5472450df1a0176fdf6d915e97cb5d9f8d3eccef7d25ff0a8871de","created_at":1681932060,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"https://nostr.build/p/nb5753.png","sig":"9df6e3872f420ed5225aaa242f1c806b4739f17e265f02ca593a3c31c5fb5cb48705ee6bc0510f81f123527aa96ec4d07adf829362e2196bd7b833deded4c2b8"}, - {"id":"e2d8aaed336d3c0f73a9ca46a89fdb2da62a6d172936a91b0067a68797b3bcb8","pubkey":"50de492cfe5472450df1a0176fdf6d915e97cb5d9f8d3eccef7d25ff0a8871de","created_at":1681931869,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Very Cool, I started playing around!","sig":"5980b2d0ef114d50c5f45b5d0c55bfc56b16a0a1807774ec2a86c4e352e17e3ec78877c73f2fedfbfd097a018df8e965107d6efd4ad9c0a0e1ecc89332d51cec"}, - {"id":"b92e4d6a5d0e8d1d2d2421044b84a4d11f2188261c55145d782b1b6bf0995009","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681932193,"kind":1,"tags":[["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f","","root"],["e","e2d8aaed336d3c0f73a9ca46a89fdb2da62a6d172936a91b0067a68797b3bcb8","","reply"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"]],"content":"love seeing it!!!","sig":"47850c205f1fa6d8836cda347c7d12c4307d35a110629eac48b4fff26600a14a5c5c8a7877b851db062a89e0157be112eae60ba0594525c1cc4d355906368dbb"}, - {"id":"da13e14cc8bcc243e0373dde14533d3829b8b621e214ca3c99c90f3dd9e11b8a","pubkey":"b488b594d84bb3e7c8421a4b91b19a006dc13d0c00da304a0263355d02195f04","created_at":1681931637,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Great thoughts ","sig":"34c81430952508746fd5d5a4c4f7df0f5762aeb17d075a74f7b0daf387a7b8772edf4152731cff6650219e1aa9f8f63924410baa03091a698af0513cbf57e0af"}, - {"id":"b5234d90a1543ba60765c57ac3fc7140129a4ac28bbd013531ec9b85e256ea55","pubkey":"b488b594d84bb3e7c8421a4b91b19a006dc13d0c00da304a0263355d02195f04","created_at":1681931560,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"I like comments ","sig":"6915f4c2a08622339957495931eabc37f8d0c50ab1f3ec42327b67d15e50f50ff4bc302848b606e6d7c170696f8d744e6f896223f30ecb39a3adffe58020ff6f"}, - {"id":"fc4e4a1230b002e4ae08251a0b26107de7f518800188b7a504f5de62d8b07996","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681834615,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is exactly why I built this. I am enamored by this idea.","sig":"bdd1b3532efa3badeb3807fe74d31c25f17c84bbe39fe3c692d3d73efa39408089843ebb3c049840f2d525e6849f8254179a888bedc776ee7ad5e6997dc10c44"}, - {"id":"d4a0b4f08d98d82a04292654ec132723cc2cf3fa24ffb6c0833426cb9372f4d5","pubkey":"39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868","created_at":1680618462,"kind":1,"tags":[["e","44d3ecbb5752e96becc330d9b56352ea595d37b4c55edd8701fd24f18df5eee2"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Unfortunately when boosting this it gets broken 🥹\n#[1]","sig":"c4a4197df4fec008f2f565170d09c46f0b9a7217ed90432cb53d1dddf70ad8520722b6e4f04ec9e52dd219ee01387c6191f449cd10a6c2488b1d2866424b7a8a"}, - {"id":"8cdc4676aca93bbafcfbe6784f9b2df54e8ca20fbe69ba55fda487736bfdb7f6","pubkey":"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93","created_at":1680636068,"kind":1,"tags":[["e","44d3ecbb5752e96becc330d9b56352ea595d37b4c55edd8701fd24f18df5eee2"],["e","d4a0b4f08d98d82a04292654ec132723cc2cf3fa24ffb6c0833426cb9372f4d5"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"we early ","sig":"b8026e43c61b491014915caf79289968b87e52aa87a487ebecbf6cb2b3c07d460f2ab1a86da99b1c5410a0f5d14e77cf27f78c56bf1ab709f0d616ad2711de78"}, - {"id":"e15b386824fbfdcbf1b50b8860f03062cef534a3ea5339cc837536fb2a58465e","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025613,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test 3","sig":"1a34f9bb9eca624f4e0c3de2ad4ad7d9c915cd16e3f1e41066a69d3799a5bb19689a8e0d894e00649cae7f7d27beadbab2b04d4083f7f2ce68e5ee65af229f55"}, - {"id":"ba9a8a1a8afb0b53fb5d4fa3f5130fe557a8d8d56fac7af9ad3443531d2a2933","pubkey":"95181f816dc80a141e9158a2a398bec37f580f4309633321a1ca53757bf08794","created_at":1690373626,"kind":1,"tags":[["e","e15b386824fbfdcbf1b50b8860f03062cef534a3ea5339cc837536fb2a58465e"],["p","d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9"]],"content":"I see your tests. 1-3 🫡","sig":"cc8d3af65578cc2436878fb5dbf3ac27751b50f91b02dd944094e9f9f2965193e32fb20ce68cfe5555ecfb4b13fd06b9cdb673dc71e5db47199b8e6cde2d2e60"}, - {"id":"b0425132a3dd4142a0f78986166aaa28021cc8fb440c95c321a95afce3d5e056","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025593,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test 2","sig":"0226d5836991836ba105cd483def0baa4255bad19a8213ae483c0168efb4de6e493233ed1fdc83e038d5227e0254dc0b461e7a7fa8032a7eada56b9840e8762f"}, - {"id":"e9bb4e2d56bd2be2952570bd52b102c23444a62ada5b78ba086f086d9147651a","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025573,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test","sig":"ba66f1123e93a70645f3f01814378d23609cc2fda38f083410e0e06b037dc9fc45d139b690e2e892278f768c97a03e02e0a9cd0a8b385271db180e1e8838784d"}, - {"id":"4d7b21c462f3fbf27a1882dbaaec4e99e5a5d18a2c18d1f2d1f0736684ad157c","pubkey":"84142dede040f7e0852ba867cfa921986b7b7e92c4b7bc5c72fbd2a4577545e4","created_at":1688308094,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"testing","sig":"9f991643229822577c967d6d71831a6d98993dbbd3613b04abda0907a944b8eb20f6f8477bc8e922308e2e2135243ab78a31a93f285ee6215ac5536141a7d95d"}, - {"id":"9cdbced750e6b1e1274b7df47cb433f565414dd08c897167eba761eadee841cc","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687990252,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"Running zapthreads","sig":"1cd2209548f5a791807e4a8aeafbf3a2fb28cee013ad5f444f86dffd448668c05480d7714b892314f5fa50b7c65a23708362c1d5b0cb669c64cfb06ca8486ba9"}, - {"id":"5799aac7a9b06f3cae3d3791b79df29d14173515cfdbf34398aab73ed4a44121","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687996513,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","9cdbced750e6b1e1274b7df47cb433f565414dd08c897167eba761eadee841cc","","reply"]],"content":"Testing nested comments","sig":"335f9969d23e28fef45adfda63da236f1d4d72dfb5873fc78961d47ee948e9333632ce8a45df3c09fc431cf8d29d2fb99bea9c9a4b3502465b05a0328d563231"}, - {"id":"7a18dda355525d468b31bba4fa947cba98cc19048d4a3099d5e9ba045d878c26","pubkey":"45c41f21e1cf715fa6d9ca20b8e002a574db7bb49e96ee89834c66dac5446b7a","created_at":1680618145,"kind":1,"tags":[["e","1a1fe6c838400f3347f97a9adce08c10068f3bc35a279520c911b025c387b061",""],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",""],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599",""],["title","Purple Text, Orange Highlights"],["summary","Using nostr as a base layer to create, consume, and monetize long-form content."],["image","https://dergigi.com/assets/images/reader.jpg"],["published_at","1680613802"]],"content":"good read. the highlighted part to be a shareable event 1 idea seems interesting, might explore it on flycat","sig":"ee913e997b907a03300616c36cffa12d2362932bd5e249cde39a44ec1250c7622703dddac167b093c3e3a28d41ebfeb9aed9d663d82bea9f8deaed4e342248dc"}, - {"id":"a3b3825af621727f9af3bd77392fe38c04d71658024916af7fe4c5867ef73eaa","pubkey":"619af6a60b3fe4c733aaca061c522cc9c7cf1d87ef4c908facc5ed936d3bdf23","created_at":1681941926,"kind":1,"tags":[["p","330fb1431ff9d8c250706bbcdc016d5495a3f744e047a408173e92ae7ee42dac"],["e","d7ef8220e3521779e2e0c3ff0afb9738f241e44ace8ed1d5237cab36330a69d0"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Good or bad time?","sig":"5629e21769b018c7f050ad35e20c3926e5f9c415ebcd54656e874cff7948a9117de6fb32d18f993b7c96f480f3e4baffb079a3db7c126c74b0188006bba41768"}, - {"id":"c5ad64b1b72776a068c39f4549d089032432814a146849eba0650d1b329fb285","pubkey":"126df36019a793922eae86bfcfedc5240b68d67953892237e3eb500ab92140bf","created_at":1681911407,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","f2610be11393f884c020119316d3304bc06c48ab01f9cbcf5a5a8290031bf20e"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Cool","sig":"62240add277f55e896ae24276fdae60f1580be9de4889e546fd2141b73e07b3e48f225a57914a5e0e5a2fd87570a1dfe1af4f869b355a7c455aa77f395c10bdb"}, - {"id":"6a58f8315af5badb1bdaeb5489417b94621a4d8e192ae2fedcca0c5dcf0c9cd4","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821926,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","81a6379301d7b517f96cc7d070b5b30fb43db97c5c814be3bc9f58d8fcbaf190"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 5","sig":"eeba84f251d8d79e0f16213b766aee8079a3a5018407c3f7d1e8161c762e6a735bb695ac54898e04d7abe245ec12f24d680927351d37a6f31557344adeebd3c2"}, - {"id":"6e9bb03c7c40d67fec0d0bb872548ec207ba0ac4533efa137d7bcaca9fb4b191","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681755803,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","9defafc0783f16b2ef9ac02d2e808a103d5d38f12905b7840420634af67d0821"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test1","sig":"17c2489ee28dc32df68ebf87ced6f5f9af0e3f091b31298fece408acba43236bc6c9f50d756f61bcf46a204e043141535e0cd281d541146cbfac259814cd2cbe"}, - {"id":"e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648013,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","81a6379301d7b517f96cc7d070b5b30fb43db97c5c814be3bc9f58d8fcbaf190"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"powerful quote 👀","sig":"eda9e5565000892dacab78469936d96aad7fb0dc0828630563b8ccb731f08e704eacfefff246b6bb668332e046f5650f2a9e4186c108f07cba07eefc775c6c58"}, - {"id":"45d4fc726f2cc5b524be862c14fdadc1a24b25b8c6c011eedf2d2909589263e7","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821952,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 7","sig":"e2c35766479d549700f4dbaeae385cfdacee1275ddf24777643ca7d7335c6d1baf7f89ac25d1dc90688bc49f8d2f5e4857d2234035291628a0c0ac760485bab0"}, - {"id":"7a4a2419824669f07081abe2132f8cc0027efbce066ccdf187c897bb7ffa5dc3","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821936,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 5","sig":"875339b3e666fb1013d6563552997dcdd02e87b1f290ad25b0d7dd085d2f87dd10a4e6c20dfe3fc109d1809733b93675505341fe149e4399c42371c743fafc73"}, - {"id":"674c62f84afdc045bc3623ea132d90afdfe4b64249807f65302231115af5406d","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648340,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"nested reply","sig":"f0f4fdd2d34e59255137c457b9d42fa9b5357e824f3154340de964616f6daf21e3b03491363b0060f97c5061445d7b2854eed328c5cced751b2b480e5ce8ce9a"}, - {"id":"d54761f672669ea4f4b7592f3b0a30ee28de340b0a7e46b91af94e66905171c9","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648345,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","674c62f84afdc045bc3623ea132d90afdfe4b64249807f65302231115af5406d"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"another one","sig":"dd732eb380542dd8068449d5617bb8b6c1dfe1495c9facbd46aad3ca9b084122479df30861b8e85e13e7b0c9bf9bce589b7fc062daa086c1ff8a0b1d1ffc7aa7"}, - {"id":"00813a18ac9084cd0948c27027a980e34039a3011f30279a8b52ad87da5a3031","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648351,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","d54761f672669ea4f4b7592f3b0a30ee28de340b0a7e46b91af94e66905171c9"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"yet another one","sig":"6332c8a441439487898fd5b423e42dde7d1d7aadfcf9b36defd78d76d1685ba099ac86ae00a1dac6ed5e8a166422d59bbb93bfccb2596dc1c5461efcfcf345b4"}, - {"id":"87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687990701,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","00813a18ac9084cd0948c27027a980e34039a3011f30279a8b52ad87da5a3031","","reply"]],"content":"and another one more","sig":"6a385fb259ff40d9b6bd8bb1d5a2e20057ec1c5b5ed2c307fbc40e4a96d4c191b80b6ea3f3f5748c907d4b0e6e64a92210a0be2889af856ad084ec6806f3cbd8"}, - {"id":"512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","pubkey":"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194","created_at":1688194800,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8","","reply"]],"content":"another one","sig":"b4bc4d8206a08de0a918043129b27c8863d17f60e4f58b4d2b535326625870d32640b04f247eb9eb5df1d62fe98c8e1b0341bdd119655553488300ec9d5b4036"}, - {"id":"ce6e32e3e17b6901d2cc70b60f3743e24f885bb6e9da6d88cff516079eac1883","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1688234921,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","","reply"]],"content":"yet another one, testing 0.0.3","sig":"13bd41c4029c6a7ee41cb03d12e831e9e9b6e14d43a61c78a72657070b45385ba2ce98e8121049fbcbdb7b2dd777c36c7867ba70b5e6a4798a9b866910ae5b62"} - ] - """.trimIndent() + companion object { + val keyPair = KeyPair() + val account = + Account( + settings = AccountSettings(keyPair = keyPair), + signer = NostrSignerInternal(keyPair), + scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), + geolocationFlow = MutableStateFlow(LocationState.LocationResult.Loading), + cache = LocalCache, + client = + NostrClient( + OkHttpWebSocket.Builder { + OkHttpClient + .Builder() + .followRedirects(true) + .followSslRedirects(true) + .build() + }, + CoroutineScope(Dispatchers.IO + SupervisorJob()), + ), + ) - val header = - """ - { - "content": "Not too long ago, I tried to paint a picture of what\na [vision for a value-enabled web][vew]\ncould look like. Now, only a couple of months later,\nall this stuff is being built. On nostr, and on lightning. Orange and\npurple, a match made in heaven.\n\nIt goes without saying that I'm beyond delighted. What a time to be alive!\n\n## nostr\n\nHere's the thing that nostr got right, and it's the same thing that\nBitcoin got right: information is easy to spread and hard to stifle.[^fn-stifle]\nInformation can be copied quickly and perfectly, which is, I believe,\nthe underlying reason for its desire to be free.\n\n[^fn-stifle]: That's a [Satoshi quote][stifle], of course: \"Bitcoin's solution is to use a peer-to-peer network to check for double-spending. In a nutshell, the network works like a distributed timestamp server, stamping the first transaction to spend a coin. It takes advantage of the nature of information being easy to spread but hard to stifle.\"\n\n[stifle]: https://satoshi.nakamotoinstitute.org/posts/p2pfoundation/1/\n\nEasy to spread, hard to stifle. That's the base reality of the nature\nof information. As always, the smart thing is to work with nature, not\nagainst it.[^1] That's what's beautiful about the orange coin and\nthe purple ostrich: both manage to work with the peculiarities of\ninformation, not against them. Both realize that information can and should be\ncopied, as it can be perfectly read and easily spread, always. Both understand\nthat resistance to censorship comes from writing to many places, making the cost\nof deletion prohibitive.\n\n> Information does not just want to be free,\n> it longs to be free. Information expands to fill the available\n> storage space. Information is Rumor's younger, stronger cousin;\n> Information is fleeter of foot, has more eyes, knows more, and\n> understands less than Rumor.\n>\n> Eric Hughes, [A Cypherpunk's Manifesto][manifesto]\n\n[manifesto]: https://nakamotoinstitute.org/static/docs/cypherpunk-manifesto.txt\n\nNostr is quickly establishing itself as a base layer for information exchange,\none that is identity-native and value-enabled. It is distinctly different from\nsystems that came before it, just like Bitcoin is distinctly different from\nmonies that came before it.\n\nAs of today, the focus of nostr is mostly on short text notes, the so-called\n\"type 1\" events more commonly known as *tweets*.[^fn-kinds] However, as you should be aware\nby now, nostr is way more than just an alternative to twitter. It is a new\nparadigm. Change the note kind from `1` to `30023` and you don't have an\nalternative to Twitter, but a replacement for Medium, Substack, and all the\nother long-form platforms. I believe that special-purpose clients that focus on\ncertain content types will emerge over time, just like we have seen the\nemergence of special-purpose platforms in the Web 2.0 era. This time, however,\nthe network effects are cumulative, not separate. A new paradigm.\n\nLet me now turn to one such special-purpose client, a nostr-based reading app.\n\n[^fn-kinds]: Refer to the various NIPs to discover the multitude of [event kinds][kinds] defined by the protocol.\n\n[kinds]: https://github.com/nostr-protocol/nips#event-kinds\n[nip23]: https://github.com/nostr-protocol/nips/blob/master/23.md\n\n## Reading\n\nI'm constantly surprised that, even though most people do read a lot\nonline, very few people seem to have a reading workflow or reading\ntools.\n\nWhy that is is anyone's guess, but maybe the added value of such tools\nis not readily apparent. You can just read the stuff right there, on the\nad-ridden, dead-ugly site, right? Why should you sign up for another\nsite, use another app, or bind yourself to another closed platform?\n\nThat's a fair point, but the success of Medium and Substack shows that\nthere is an appetite for clean reading and writing, as well as providing\navenues for authors to get paid for their writing (and a willingness of\nreaders to support said authors, just because).\n\nThe problem is, of course, that all of these platforms are *platforms*,\nwhich is to say, walled gardens that imprison readers and writers alike.\nWorse than that: they are fiat platforms, which means that\npermissionless value-flows are not only absent from their DNA, they are\noutright impossible.[^2]\n\nNostr fixes this.\n\n![Nostriches like to read, or so I've heard](https://dergigi.com/assets/images/bitcoin/2023-04-04-purple-text-orange-highlights/nostrich-reading-a-newspaper.jpg)\n\nThe beauty of nostr is that it is not a platform. It's a protocol,\nwhich means that you don't have to sign up for it---you can create an\nidentity yourself. You don't have to ask for permission; you just *do*,\nwithout having to rely on the benevolence of whatever dictator is in\ncharge of the platform right now.\n\nNostr is *not* a platform, and yet, powerful tools and services can be\nbuilt and monetized on top of it. This is good for users, good for\nservice providers, and good for the network(s) at large. Win-win-win.\n\nSo what am I talking about, exactly? How can nostr improve everyone's\nreading (and writing) experience?\n\nAllow me to paint a (rough) picture of what I have in mind. Nostr\nalready supports private and public bookmarks, so let's start from\nthere.\n\nImagine a special-purpose client that scans all your bookmarks for long-form\ncontent.[^fn-urls] Everything that you marked to be read later is shown in an orderly\nfashion, which is to say searchable, sortable, filterable, and displayed without\ndistractions. Voilà, you have yourself a reading app. That's, in essence, how\nPocket, Readwise, and other reading apps work. But all these apps are walled\ngardens without much interoperability and without direct monetization.\n\n[^fn-urls]: In the nostr world long-form content is simply markdown as defined in [NIP-23][nip23], but it could also be a link to an article or PDF, which in turn could get [converted into markdown][readability] and posted as an event to a special relay.\n\n[readability]: https://github.com/mozilla/readability\n\nBitcoin fixes the direct monetization part.[^fn-v4v] Nostr fixes the interoperability part.\n\n[^fn-v4v]: ...because Bitcoin makes [V4V][busking] practical. (Paywalls are not the way.)\n\nAlright, we got ourselves a boring reading app. Great. Now, imagine that\nusers are able to highlight passages. These highlights, just like\nbookmarks now, could be private or public. When shared publicly,\nsomething interesting emerges: an overlay on existing content, a lens on\nthe written Web. In other words: *swarm highlights*.\n\nImagine a visual overlay of all public highlights, automatically shining\na light on what the swarm of readers found most useful, insightful,\nfunny, etc.\n\n![Swarm Highlights](https://dergigi.com/assets/images/bitcoin/2023-04-04-purple-text-orange-highlights/highlights.png)\n\nFurther, imagine the possibility of sharing these highlights as a \"type 1\" event\nwith one click, automatically tagging the highlighter(s)---as well as the\nauthor, of course---so that eventual sat-flows can be split and forwarded\nautomatically.\n\n![Automated value splits](https://dergigi.com/assets/images/bitcoin/2023-04-04-purple-text-orange-highlights/sat-flows.png)\n\nVoilà, you have a system that allows for value to flow back to those who\nprovide it, be it authors, editors, curators, or readers that willingly\nslog through the information jungle to share and highlight the best\nstuff (which is a form of curation, of course).\n\nZaps make nostr a defacto address book[^fn-pp] of payment information, which is\nto say lightning addresses, as of now. Thanks to [nostr wallet connect][nwc] (among\nother developments), sending sats ~~will soon be~~ is already as\nfrictionless as leaving a like.\n\n[^fn-pp]: The Yellow Pages are dead, long live [The Purple Pages](http://purplepag.es/)!\n\nValue-for-value and participatory payment flows are something that\ntraditional reading apps desperately lack, be it Pocket, Instapaper,\nReadwise, or the simple reading mode that is part of every browser.\n\nA neat side-effect of a more structured way to share passages of text is\nthat it enables semi-structured discussions around said\npassages---which could be another useful overlay inside\nspecial-purpose clients, providing context and further insights.[^5]\n\nFurther, imagine the option of seamlessly switching from text-on-screen\nto text-to-speech, allowing the user to stream sats if desired, as\nPodcasting 2.0 clients already do.[^3]\n\nImagine user-built curations of the best articles of the week, bundled\nneatly for your reading pleasure, incentivized by a small value split\nthat allows the curator to participate in the flow of sats.\n\nYou get the idea.\n\nI'm sure that the various implementation details will be hashed out,\nbut as I see it, 90% of the stuff is already there. Maybe we'll need\nanother NIP or two, but I don't see a reason why this can't be\nbuilt---and, more importantly: I don't see a reason why it wouldn't\nbe sustainable for everyone involved.\n\nMost puzzle pieces are already there, and the rest of them can probably\nbe implemented by custom event types. From the point of view of nostr,\nmost everything is an event: bookmarks are events, highlights are\nevents, marking something as read is an event, and sharing an excerpt or\na highlight is an event. Public actions are out in the open, private\nactions are encrypted, the data is not in a silo, and everyone wins.\nEspecially the users, those who are at the edge of the network and\nusually lose out on the value generated.\n\nIn this case, the reading case, the users are mostly \"consumers\" of\ncontent. What changes from the producing perspective, the perspective of\nthe writer?\n\n## Writing\n\nBack to the one thing that nostr got right: information is easy to\nspread but hard to stifle. In addition to that, digital information can\nbe copied perfectly, which is why it shouldn't matter where stuff is\npublished in the first place.\n\nAllow me to repeat this point in all caps, for emphasis: **IT SHOULD NOT\nMATTER WHERE INFORMATION IS PUBLISHED**, and, maybe even more\nimportantly, it shouldn't matter if it is published in a hundred\ndifferent places at once.[^fn-torrents]\n\nWhat matters is trust and accuracy, which is to say, digital signatures\nand reputation. To translate this to nostr speak: because every event is\nsigned by default, as long as you trust the person behind the signature,\nit doesn't matter from which relay the information is fetched.\n\nThis is already true (or mostly true) on the regular web. Whether you\nread the internet archive version of an article or the version that is\npublished by an online magazine, the version on the author's website,\nor the version read by some guy that has read more about Bitcoin than\nanyone else you know[^fn-guy]---it's all the same, essentially. What matters\nis the information itself.\n\n[^fn-guy]: There is only one such guy, as we all know, and it's this Guy: nostr:npub1h8nk2346qezka5cpm8jjh3yl5j88pf4ly2ptu7s6uu55wcfqy0wq36rpev\n\nPractically speaking, the source of truth in a hypernostrized world is---you\nguessed it---an event. An event signed by the author, which allows for\nthe information to be wrapped in a tamper-proof manner, which in turn\nallows the information to spread far and wide---without it being\nhosted in one place.\n\nThe first clients that focus on long-form content already exist, and I expect\nmore clients to pop up over time.[^4] As mentioned before, one could easily\nimagine [prism-like value splits][prism] seamlessly integrated into these\nclients, splitting zaps automatically to compensate writers, editors,\nproofreaders, and illustrators in a V4V fashion. Further, one could imagine\nvarious compute-intensive services built into these special-purpose clients,\nsuch as GPT Ghostwriters, or writing aids such as Grammarly and the like. All\nthese services could be seamlessly paid for in sats, without the requirement of\nany sign-ups or the gathering of any user data. That's the beauty of [money\nproper][rediscovery].\n\n![A clean and simple reading and writing interface](https://dergigi.com/assets/images/bitcoin/2023-04-04-purple-text-orange-highlights/nostr-reader-and-writer.png)\n\nPlagiarism is one issue that needs to be dealt with, of course. Humans\nare greedy, and some humans are assholes. Neither bitcoin nor nostr\nfixes this. However, while plagiarism detection is not necessarily\ntrivial, it is also not impossible, especially if most texts are\npublished on nostr first. Nostr-based publishing tools allow for\nOpenTimestamp attestations thanks\nto [NIP-03](https://github.com/nostr-protocol/nips/blob/master/03.md),\nwhich in turn allows for plagiarism detection based on \"first seen\"\nlookups.\n\nThat's just one way to deal with the problem, of course. In any case,\nI'm confident that we'll figure it out.\n\n## Value\n\nI believe that in the open ~~attention~~ information economy we find\nourselves in, value will mostly derive from effective curation,\ndissemination, and transmission of information, *not* the exclusive\nownership of it.\n\nAlthough it is still early days,\nthe [statistics](https://stats.podcastindex.org/v4v) around Podcasting\n2.0 and [nostr zaps](https://zaplife.lol/) clearly show that (a) people\nare willing to monetarily reward content they care about, and (b) the\nwillingness to send sats *increases* as friction *decreases*.\n\nThe ingenious thing about boostagrams and zaps is that they are direct\nand visible, which is to say, public and interactive. They are neither\nregular transactions nor simple donations---they are something else\nentirely. An unforgable value signal, a special form of gratitude and\nappreciation.\n\nContrast that with a link to Paypal or Patreon: impersonal, slow,\nindirect, and friction-laden. It's the opposite of a super-charged\ninteraction.\n\nWhile today's information jungle increasingly presents itself in the\nform of (short) videos and (long-form) audio, I believe that we will see\na renaissance of the written word, especially if we manage to move away\nfrom an economy built around attention, towards an economy built upon\nvalue and insight.\n\nThe orange future now has a purple hue, and I believe that it will be as\nbright as ever. We just have a lot of building to do.\n\n---\n\n## Further Reading\n\n- [A Vision for a Value-Enabled Web][vew]\n- [The Freedom of Value][busking]\n- [The Rediscovery of Money][prism]\n- [Lightning Prisms][rediscovery]\n\n[vew]: https://dergigi.com/vew\n[prism]: https://dergigi.com/prism\n[rediscovery]: https://dergigi.com/rediscovery\n[busking]: https://dergigi.com/busking\n\n## NIPs and Resources\n\n- [Nostr Resources][nr]\n- [value4value.info](https://value4value.info/)\n- [nips.be](https://nips.be/)\n- [NIP-23: Long-form content](https://github.com/nostr-protocol/nips/blob/master/23.md)\n- [NIP-57: Event-specific zap markers](https://github.com/nostr-protocol/nips/blob/master/57.md)\n- [NIP-47: Nostr Wallet Connect](https://github.com/getAlby/nips/blob/master/47.md)\n- [NIP-03: OpenTimestamps attestations for events](https://github.com/nostr-protocol/nips/blob/master/03.md)\n\nOriginally published on [dergigi.com](https://dergigi.com/reader)\n\n---\n\n[^1]: Paywalls work against this nature, which is why I consider them misguided at best and incredibly retarded at worst.\n\n[^2]: Fiat doesn't work for the [value-enabled web][vew], as fiat rails can never be open and permissionless. Digital fiat is never money. It is---and always will be---[credit][rediscovery].\n\n[^3]: Whether the recipient is a text-to-speech service provider or a human narrator doesn't even matter too much, sats will flow just the same.\n\n[^4]: [BlogStack](https://blogstack.io/) and [Habla](https://habla.news/) being two of them.\n\n[^5]: Use a URI as the discussion base (instead of a highlight), and you got yourself a [Disqus](https://disqus.com/) in purple feathers!\n\n[^fn-torrents]: That's what torrents got right, and [ipfs] for that matter.\n\n[nr]: https://nostr-resources.com\n[nwc]: https://nwc.getalby.com/\n[ipfs]: https://fiatjaf.com/d5031e5b.html\n", - "created_at": 1680614039, - "id": "9517aa60334990596fdea4493c1bde429c55e9cefb84d8d88a6758c4e3ebcabc", - "kind": 30023, - "pubkey": "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", - "sig": "c8c8c7ac8c288f6a6e1090e0e5f7eed85daaaba03eeab0a4936535f95ce066c08723bedcfb80a6af4941a9fafb9fe351950b9376d13ff0e81030cd4391840f04", - "tags": [ - ["d","1680612926599"], - ["title","Purple Text, Orange Highlights"], - ["summary","Using nostr as a base layer to create, consume, and monetize long-form content."], - ["published_at","1680613802"], - ["t","nostr"], - ["t","reading"], - ["t","writing"], - ["t","readability"], - ["t","highlights"], - ["t","pocket"], - ["t","instapaper"], - ["t","readwise"], - ["t","disqus"], - ["t","v4v"], - ["t","value4value"], - ["image","https://dergigi.com/assets/images/reader.jpg"], - ["p","b9e76546ba06456ed301d9e52bc49fa48e70a6bf2282be7a1ae72947612023dc"] - ] - } - """.trimIndent() + val db = + """ + [ + {"id":"741f5367a9415f4d6f19c0f57a1e4647c8ed8309b53b0da2d82fc4ebfba03b2c","pubkey":"d85c99afd244911e0aaf800cbea4221df557f06f8a4ff2cbe84b24e0b9e728fc","created_at":1684674845,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Getting my head around this","sig":"069874040bac26a219777fc0f90b8f4df71e38c30e3e6a953d53222499d8e0c5a8f32c6b4204d14eb335bb654f01c5610372d9dc00062284b8e0f2bb98c7ed85"}, + {"id":"22323fc72b4c37f93ea21f6069684339ce5f63111161c81f2aa3de4a21bfe83b","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1683571810,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test","sig":"f6d3bcf0f8e07d06720a2527f95910ee5a66aa889dd9261514921a155bbc3e9a3c7d721f5a2ec948cd795183f85dbe80f2ee2e36663f9d1de33fc03274ccb9d9"}, + {"id":"14525fcaae530a029f782fd361dd0cd66634c3e23020bd19e66fe11c1e254e32","pubkey":"afd563434a737334d69db899e4a32fe38d73a182bb6d1e91d83a2c4c4e04737c","created_at":1682968749,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is most amazing thing yet for nostr!","sig":"3dcbb058b8fa6e6f388f5b1a26d21b888dbc91bb831b0ed80ee30d9d2494fbbbb40dac9daf637b948c3afe1f32c2c662eca8d7acc0ba31f23b89edc8ade99631"}, + {"id":"98ae0d6d10e494ed0bf70feb577e8225c6a6732c7af3d29f88bfe1b87d4439e6","pubkey":"83fd07de9b763334cc9d46f2785c2558e6c2eabfe7d0c6ec214667cbaec50d47","created_at":1681995243,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"true if big","sig":"bb7d97e81207096d8615ebb0458c3023fa1b8f3aac463f3f0293ae94e2b90d043ad371067ce984a2ab2ba49cd7432a553f128b8d2d678238551538ad657bd75c"}, + {"id":"36e262e71b7e8bcae946f69885b8c3614e318e82864437342cf50e8b9ab7229d","pubkey":"b111d517452f9ef015e16d60ae623a6b66af63024eec941b0653bfee0dd667d4","created_at":1681979196,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"this is wonderful. great work.","sig":"205a46d867c22cbb09c7dcc4351330a95304c0d80540c85b9ea39f7f464ea60df1ef5006c1f93b862536f04d8a7855e4cc2a915d7a01437cd30f7f12c716b239"}, + {"id":"37725c2924ca267d66c2c27c2dae65550c07e7b883034cf1ea69671883430642","pubkey":"90b9bec74789688e515125596ab6350bfe646176ac75742275063922c5fea010","created_at":1681944950,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Epic!","sig":"c461b97a3cdc5cf967e6f32c59c94b1b912c296feb1cbb7943c81731e558b10a3674982d6990567260d9fb033f5a3d3db8984b1eb362715d68ba46034a8b1b86"}, + {"id":"8ba54cfb6375270e8ae97a7e0992c1a0dbaa4cd46af8309d67a839e86789fde6","pubkey":"c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11","created_at":1681944377,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is incredible... Bravo, Pablo, and shoutout to Gigi.","sig":"fb1a84d4859742b001b810c98a5e9301bc861fa2d65a711cb21c57f6e62c98f27e5b0c0c6432ef1111bc6cc52aa8cb0ae909d28af50c497d125950bfaeade475"}, + {"id":"53410bc6d47e87f3f18ecbc93c716b5a6ef8ee3805516b2ff4d155154a685b7c","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681945005,"kind":1,"tags":[["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["e","8ba54cfb6375270e8ae97a7e0992c1a0dbaa4cd46af8309d67a839e86789fde6"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11"]],"content":"Thank you, my friend 🤙 ","sig":"3d2fbcefdfac2183601076c8f9e518e05620cefe74cba233684cce931a69b51befa329c94734e434805bd9382ff3dad36e42d1955d4e47c822e973ba69b7377f"}, + {"id":"e383476cb1ce5accde11d4b1338424fa32c3724cf96e6214af8e5e852981728a","pubkey":"50de492cfe5472450df1a0176fdf6d915e97cb5d9f8d3eccef7d25ff0a8871de","created_at":1681932060,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"https://nostr.build/p/nb5753.png","sig":"9df6e3872f420ed5225aaa242f1c806b4739f17e265f02ca593a3c31c5fb5cb48705ee6bc0510f81f123527aa96ec4d07adf829362e2196bd7b833deded4c2b8"}, + {"id":"e2d8aaed336d3c0f73a9ca46a89fdb2da62a6d172936a91b0067a68797b3bcb8","pubkey":"50de492cfe5472450df1a0176fdf6d915e97cb5d9f8d3eccef7d25ff0a8871de","created_at":1681931869,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Very Cool, I started playing around!","sig":"5980b2d0ef114d50c5f45b5d0c55bfc56b16a0a1807774ec2a86c4e352e17e3ec78877c73f2fedfbfd097a018df8e965107d6efd4ad9c0a0e1ecc89332d51cec"}, + {"id":"b92e4d6a5d0e8d1d2d2421044b84a4d11f2188261c55145d782b1b6bf0995009","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681932193,"kind":1,"tags":[["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f","","root"],["e","e2d8aaed336d3c0f73a9ca46a89fdb2da62a6d172936a91b0067a68797b3bcb8","","reply"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"]],"content":"love seeing it!!!","sig":"47850c205f1fa6d8836cda347c7d12c4307d35a110629eac48b4fff26600a14a5c5c8a7877b851db062a89e0157be112eae60ba0594525c1cc4d355906368dbb"}, + {"id":"da13e14cc8bcc243e0373dde14533d3829b8b621e214ca3c99c90f3dd9e11b8a","pubkey":"b488b594d84bb3e7c8421a4b91b19a006dc13d0c00da304a0263355d02195f04","created_at":1681931637,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Great thoughts ","sig":"34c81430952508746fd5d5a4c4f7df0f5762aeb17d075a74f7b0daf387a7b8772edf4152731cff6650219e1aa9f8f63924410baa03091a698af0513cbf57e0af"}, + {"id":"b5234d90a1543ba60765c57ac3fc7140129a4ac28bbd013531ec9b85e256ea55","pubkey":"b488b594d84bb3e7c8421a4b91b19a006dc13d0c00da304a0263355d02195f04","created_at":1681931560,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"I like comments ","sig":"6915f4c2a08622339957495931eabc37f8d0c50ab1f3ec42327b67d15e50f50ff4bc302848b606e6d7c170696f8d744e6f896223f30ecb39a3adffe58020ff6f"}, + {"id":"fc4e4a1230b002e4ae08251a0b26107de7f518800188b7a504f5de62d8b07996","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681834615,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","532808e4d60f5f82b95aeaa3ed2e930a0c5973dccb0ede68b28b1931db91440f"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"This is exactly why I built this. I am enamored by this idea.","sig":"bdd1b3532efa3badeb3807fe74d31c25f17c84bbe39fe3c692d3d73efa39408089843ebb3c049840f2d525e6849f8254179a888bedc776ee7ad5e6997dc10c44"}, + {"id":"d4a0b4f08d98d82a04292654ec132723cc2cf3fa24ffb6c0833426cb9372f4d5","pubkey":"39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868","created_at":1680618462,"kind":1,"tags":[["e","44d3ecbb5752e96becc330d9b56352ea595d37b4c55edd8701fd24f18df5eee2"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Unfortunately when boosting this it gets broken 🥹\n#[1]","sig":"c4a4197df4fec008f2f565170d09c46f0b9a7217ed90432cb53d1dddf70ad8520722b6e4f04ec9e52dd219ee01387c6191f449cd10a6c2488b1d2866424b7a8a"}, + {"id":"8cdc4676aca93bbafcfbe6784f9b2df54e8ca20fbe69ba55fda487736bfdb7f6","pubkey":"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93","created_at":1680636068,"kind":1,"tags":[["e","44d3ecbb5752e96becc330d9b56352ea595d37b4c55edd8701fd24f18df5eee2"],["e","d4a0b4f08d98d82a04292654ec132723cc2cf3fa24ffb6c0833426cb9372f4d5"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["p","39a8b17475be0db44e313f9fd032ffde183c8abd6498e4932a873330d2cd4868"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"we early ","sig":"b8026e43c61b491014915caf79289968b87e52aa87a487ebecbf6cb2b3c07d460f2ab1a86da99b1c5410a0f5d14e77cf27f78c56bf1ab709f0d616ad2711de78"}, + {"id":"e15b386824fbfdcbf1b50b8860f03062cef534a3ea5339cc837536fb2a58465e","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025613,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test 3","sig":"1a34f9bb9eca624f4e0c3de2ad4ad7d9c915cd16e3f1e41066a69d3799a5bb19689a8e0d894e00649cae7f7d27beadbab2b04d4083f7f2ce68e5ee65af229f55"}, + {"id":"ba9a8a1a8afb0b53fb5d4fa3f5130fe557a8d8d56fac7af9ad3443531d2a2933","pubkey":"95181f816dc80a141e9158a2a398bec37f580f4309633321a1ca53757bf08794","created_at":1690373626,"kind":1,"tags":[["e","e15b386824fbfdcbf1b50b8860f03062cef534a3ea5339cc837536fb2a58465e"],["p","d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9"]],"content":"I see your tests. 1-3 🫡","sig":"cc8d3af65578cc2436878fb5dbf3ac27751b50f91b02dd944094e9f9f2965193e32fb20ce68cfe5555ecfb4b13fd06b9cdb673dc71e5db47199b8e6cde2d2e60"}, + {"id":"b0425132a3dd4142a0f78986166aaa28021cc8fb440c95c321a95afce3d5e056","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025593,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test 2","sig":"0226d5836991836ba105cd483def0baa4255bad19a8213ae483c0168efb4de6e493233ed1fdc83e038d5227e0254dc0b461e7a7fa8032a7eada56b9840e8762f"}, + {"id":"e9bb4e2d56bd2be2952570bd52b102c23444a62ada5b78ba086f086d9147651a","pubkey":"d3d2b986796c9e2067c58fc9017cace5adc1876e9b674719078d5c13d82e54a9","created_at":1690025573,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"test","sig":"ba66f1123e93a70645f3f01814378d23609cc2fda38f083410e0e06b037dc9fc45d139b690e2e892278f768c97a03e02e0a9cd0a8b385271db180e1e8838784d"}, + {"id":"4d7b21c462f3fbf27a1882dbaaec4e99e5a5d18a2c18d1f2d1f0736684ad157c","pubkey":"84142dede040f7e0852ba867cfa921986b7b7e92c4b7bc5c72fbd2a4577545e4","created_at":1688308094,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"testing","sig":"9f991643229822577c967d6d71831a6d98993dbbd3613b04abda0907a944b8eb20f6f8477bc8e922308e2e2135243ab78a31a93f285ee6215ac5536141a7d95d"}, + {"id":"9cdbced750e6b1e1274b7df47cb433f565414dd08c897167eba761eadee841cc","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687990252,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"]],"content":"Running zapthreads","sig":"1cd2209548f5a791807e4a8aeafbf3a2fb28cee013ad5f444f86dffd448668c05480d7714b892314f5fa50b7c65a23708362c1d5b0cb669c64cfb06ca8486ba9"}, + {"id":"5799aac7a9b06f3cae3d3791b79df29d14173515cfdbf34398aab73ed4a44121","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687996513,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","9cdbced750e6b1e1274b7df47cb433f565414dd08c897167eba761eadee841cc","","reply"]],"content":"Testing nested comments","sig":"335f9969d23e28fef45adfda63da236f1d4d72dfb5873fc78961d47ee948e9333632ce8a45df3c09fc431cf8d29d2fb99bea9c9a4b3502465b05a0328d563231"}, + {"id":"7a18dda355525d468b31bba4fa947cba98cc19048d4a3099d5e9ba045d878c26","pubkey":"45c41f21e1cf715fa6d9ca20b8e002a574db7bb49e96ee89834c66dac5446b7a","created_at":1680618145,"kind":1,"tags":[["e","1a1fe6c838400f3347f97a9adce08c10068f3bc35a279520c911b025c387b061",""],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",""],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599",""],["title","Purple Text, Orange Highlights"],["summary","Using nostr as a base layer to create, consume, and monetize long-form content."],["image","https://dergigi.com/assets/images/reader.jpg"],["published_at","1680613802"]],"content":"good read. the highlighted part to be a shareable event 1 idea seems interesting, might explore it on flycat","sig":"ee913e997b907a03300616c36cffa12d2362932bd5e249cde39a44ec1250c7622703dddac167b093c3e3a28d41ebfeb9aed9d663d82bea9f8deaed4e342248dc"}, + {"id":"a3b3825af621727f9af3bd77392fe38c04d71658024916af7fe4c5867ef73eaa","pubkey":"619af6a60b3fe4c733aaca061c522cc9c7cf1d87ef4c908facc5ed936d3bdf23","created_at":1681941926,"kind":1,"tags":[["p","330fb1431ff9d8c250706bbcdc016d5495a3f744e047a408173e92ae7ee42dac"],["e","d7ef8220e3521779e2e0c3ff0afb9738f241e44ace8ed1d5237cab36330a69d0"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Good or bad time?","sig":"5629e21769b018c7f050ad35e20c3926e5f9c415ebcd54656e874cff7948a9117de6fb32d18f993b7c96f480f3e4baffb079a3db7c126c74b0188006bba41768"}, + {"id":"c5ad64b1b72776a068c39f4549d089032432814a146849eba0650d1b329fb285","pubkey":"126df36019a793922eae86bfcfedc5240b68d67953892237e3eb500ab92140bf","created_at":1681911407,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","f2610be11393f884c020119316d3304bc06c48ab01f9cbcf5a5a8290031bf20e"],["p","6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"Cool","sig":"62240add277f55e896ae24276fdae60f1580be9de4889e546fd2141b73e07b3e48f225a57914a5e0e5a2fd87570a1dfe1af4f869b355a7c455aa77f395c10bdb"}, + {"id":"6a58f8315af5badb1bdaeb5489417b94621a4d8e192ae2fedcca0c5dcf0c9cd4","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821926,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","81a6379301d7b517f96cc7d070b5b30fb43db97c5c814be3bc9f58d8fcbaf190"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 5","sig":"eeba84f251d8d79e0f16213b766aee8079a3a5018407c3f7d1e8161c762e6a735bb695ac54898e04d7abe245ec12f24d680927351d37a6f31557344adeebd3c2"}, + {"id":"6e9bb03c7c40d67fec0d0bb872548ec207ba0ac4533efa137d7bcaca9fb4b191","pubkey":"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52","created_at":1681755803,"kind":1,"tags":[["p","fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52"],["e","9defafc0783f16b2ef9ac02d2e808a103d5d38f12905b7840420634af67d0821"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test1","sig":"17c2489ee28dc32df68ebf87ced6f5f9af0e3f091b31298fece408acba43236bc6c9f50d756f61bcf46a204e043141535e0cd281d541146cbfac259814cd2cbe"}, + {"id":"e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648013,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","81a6379301d7b517f96cc7d070b5b30fb43db97c5c814be3bc9f58d8fcbaf190"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"powerful quote 👀","sig":"eda9e5565000892dacab78469936d96aad7fb0dc0828630563b8ccb731f08e704eacfefff246b6bb668332e046f5650f2a9e4186c108f07cba07eefc775c6c58"}, + {"id":"45d4fc726f2cc5b524be862c14fdadc1a24b25b8c6c011eedf2d2909589263e7","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821952,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 7","sig":"e2c35766479d549700f4dbaeae385cfdacee1275ddf24777643ca7d7335c6d1baf7f89ac25d1dc90688bc49f8d2f5e4857d2234035291628a0c0ac760485bab0"}, + {"id":"7a4a2419824669f07081abe2132f8cc0027efbce066ccdf187c897bb7ffa5dc3","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681821936,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"test 5","sig":"875339b3e666fb1013d6563552997dcdd02e87b1f290ad25b0d7dd085d2f87dd10a4e6c20dfe3fc109d1809733b93675505341fe149e4399c42371c743fafc73"}, + {"id":"674c62f84afdc045bc3623ea132d90afdfe4b64249807f65302231115af5406d","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648340,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","e2ae784b239cac4bad38136e4bd758b87dd261b659ef460450064bf9073edcb3"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"nested reply","sig":"f0f4fdd2d34e59255137c457b9d42fa9b5357e824f3154340de964616f6daf21e3b03491363b0060f97c5061445d7b2854eed328c5cced751b2b480e5ce8ce9a"}, + {"id":"d54761f672669ea4f4b7592f3b0a30ee28de340b0a7e46b91af94e66905171c9","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648345,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","674c62f84afdc045bc3623ea132d90afdfe4b64249807f65302231115af5406d"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"another one","sig":"dd732eb380542dd8068449d5617bb8b6c1dfe1495c9facbd46aad3ca9b084122479df30861b8e85e13e7b0c9bf9bce589b7fc062daa086c1ff8a0b1d1ffc7aa7"}, + {"id":"00813a18ac9084cd0948c27027a980e34039a3011f30279a8b52ad87da5a3031","pubkey":"73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc","created_at":1681648351,"kind":1,"tags":[["p","73c7f6d5bb599bb7d7cee84c72e89dbd549df53da522ed6c7611055cc0db64bc"],["e","d54761f672669ea4f4b7592f3b0a30ee28de340b0a7e46b91af94e66905171c9"],["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599"]],"content":"yet another one","sig":"6332c8a441439487898fd5b423e42dde7d1d7aadfcf9b36defd78d76d1685ba099ac86ae00a1dac6ed5e8a166422d59bbb93bfccb2596dc1c5461efcfcf345b4"}, + {"id":"87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1687990701,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","00813a18ac9084cd0948c27027a980e34039a3011f30279a8b52ad87da5a3031","","reply"]],"content":"and another one more","sig":"6a385fb259ff40d9b6bd8bb1d5a2e20057ec1c5b5ed2c307fbc40e4a96d4c191b80b6ea3f3f5748c907d4b0e6e64a92210a0be2889af856ad084ec6806f3cbd8"}, + {"id":"512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","pubkey":"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194","created_at":1688194800,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","87a5bd25aa084cefb3357fc9c2a5b327254fab35fdd7b2d4bd0acddc63d0abe8","","reply"]],"content":"another one","sig":"b4bc4d8206a08de0a918043129b27c8863d17f60e4f58b4d2b535326625870d32640b04f247eb9eb5df1d62fe98c8e1b0341bdd119655553488300ec9d5b4036"}, + {"id":"ce6e32e3e17b6901d2cc70b60f3743e24f885bb6e9da6d88cff516079eac1883","pubkey":"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11","created_at":1688234921,"kind":1,"tags":[["a","30023:6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93:1680612926599","","root"],["e","512962dbada5fd5015fc727a107d5c3f569662de67eab8e5da5a8065012cf11e","","reply"]],"content":"yet another one, testing 0.0.3","sig":"13bd41c4029c6a7ee41cb03d12e831e9e9b6e14d43a61c78a72657070b45385ba2ce98e8121049fbcbdb7b2dd777c36c7867ba70b5e6a4798a9b866910ae5b62"} + ] + """.trimIndent() + + val header = + """ + { + "content": "Not too long ago, I tried to paint a picture of what\na [vision for a value-enabled web][vew]\ncould look like. Now, only a couple of months later,\nall this stuff is being built. On nostr, and on lightning. Orange and\npurple, a match made in heaven.\n\nIt goes without saying that I'm beyond delighted. What a time to be alive!\n\n## nostr\n\nHere's the thing that nostr got right, and it's the same thing that\nBitcoin got right: information is easy to spread and hard to stifle.[^fn-stifle]\nInformation can be copied quickly and perfectly, which is, I believe,\nthe underlying reason for its desire to be free.\n\n[^fn-stifle]: That's a [Satoshi quote][stifle], of course: \"Bitcoin's solution is to use a peer-to-peer network to check for double-spending. In a nutshell, the network works like a distributed timestamp server, stamping the first transaction to spend a coin. It takes advantage of the nature of information being easy to spread but hard to stifle.\"\n\n[stifle]: https://satoshi.nakamotoinstitute.org/posts/p2pfoundation/1/\n\nEasy to spread, hard to stifle. That's the base reality of the nature\nof information. As always, the smart thing is to work with nature, not\nagainst it.[^1] That's what's beautiful about the orange coin and\nthe purple ostrich: both manage to work with the peculiarities of\ninformation, not against them. Both realize that information can and should be\ncopied, as it can be perfectly read and easily spread, always. Both understand\nthat resistance to censorship comes from writing to many places, making the cost\nof deletion prohibitive.\n\n> Information does not just want to be free,\n> it longs to be free. Information expands to fill the available\n> storage space. Information is Rumor's younger, stronger cousin;\n> Information is fleeter of foot, has more eyes, knows more, and\n> understands less than Rumor.\n>\n> Eric Hughes, [A Cypherpunk's Manifesto][manifesto]\n\n[manifesto]: https://nakamotoinstitute.org/static/docs/cypherpunk-manifesto.txt\n\nNostr is quickly establishing itself as a base layer for information exchange,\none that is identity-native and value-enabled. It is distinctly different from\nsystems that came before it, just like Bitcoin is distinctly different from\nmonies that came before it.\n\nAs of today, the focus of nostr is mostly on short text notes, the so-called\n\"type 1\" events more commonly known as *tweets*.[^fn-kinds] However, as you should be aware\nby now, nostr is way more than just an alternative to twitter. It is a new\nparadigm. Change the note kind from `1` to `30023` and you don't have an\nalternative to Twitter, but a replacement for Medium, Substack, and all the\nother long-form platforms. I believe that special-purpose clients that focus on\ncertain content types will emerge over time, just like we have seen the\nemergence of special-purpose platforms in the Web 2.0 era. This time, however,\nthe network effects are cumulative, not separate. A new paradigm.\n\nLet me now turn to one such special-purpose client, a nostr-based reading app.\n\n[^fn-kinds]: Refer to the various NIPs to discover the multitude of [event kinds][kinds] defined by the protocol.\n\n[kinds]: https://github.com/nostr-protocol/nips#event-kinds\n[nip23]: https://github.com/nostr-protocol/nips/blob/master/23.md\n\n## Reading\n\nI'm constantly surprised that, even though most people do read a lot\nonline, very few people seem to have a reading workflow or reading\ntools.\n\nWhy that is is anyone's guess, but maybe the added value of such tools\nis not readily apparent. You can just read the stuff right there, on the\nad-ridden, dead-ugly site, right? Why should you sign up for another\nsite, use another app, or bind yourself to another closed platform?\n\nThat's a fair point, but the success of Medium and Substack shows that\nthere is an appetite for clean reading and writing, as well as providing\navenues for authors to get paid for their writing (and a willingness of\nreaders to support said authors, just because).\n\nThe problem is, of course, that all of these platforms are *platforms*,\nwhich is to say, walled gardens that imprison readers and writers alike.\nWorse than that: they are fiat platforms, which means that\npermissionless value-flows are not only absent from their DNA, they are\noutright impossible.[^2]\n\nNostr fixes this.\n\n![Nostriches like to read, or so I've heard](https://dergigi.com/assets/images/bitcoin/2023-04-04-purple-text-orange-highlights/nostrich-reading-a-newspaper.jpg)\n\nThe beauty of nostr is that it is not a platform. It's a protocol,\nwhich means that you don't have to sign up for it---you can create an\nidentity yourself. You don't have to ask for permission; you just *do*,\nwithout having to rely on the benevolence of whatever dictator is in\ncharge of the platform right now.\n\nNostr is *not* a platform, and yet, powerful tools and services can be\nbuilt and monetized on top of it. This is good for users, good for\nservice providers, and good for the network(s) at large. Win-win-win.\n\nSo what am I talking about, exactly? How can nostr improve everyone's\nreading (and writing) experience?\n\nAllow me to paint a (rough) picture of what I have in mind. Nostr\nalready supports private and public bookmarks, so let's start from\nthere.\n\nImagine a special-purpose client that scans all your bookmarks for long-form\ncontent.[^fn-urls] Everything that you marked to be read later is shown in an orderly\nfashion, which is to say searchable, sortable, filterable, and displayed without\ndistractions. Voilà, you have yourself a reading app. That's, in essence, how\nPocket, Readwise, and other reading apps work. But all these apps are walled\ngardens without much interoperability and without direct monetization.\n\n[^fn-urls]: In the nostr world long-form content is simply markdown as defined in [NIP-23][nip23], but it could also be a link to an article or PDF, which in turn could get [converted into markdown][readability] and posted as an event to a special relay.\n\n[readability]: https://github.com/mozilla/readability\n\nBitcoin fixes the direct monetization part.[^fn-v4v] Nostr fixes the interoperability part.\n\n[^fn-v4v]: ...because Bitcoin makes [V4V][busking] practical. (Paywalls are not the way.)\n\nAlright, we got ourselves a boring reading app. Great. Now, imagine that\nusers are able to highlight passages. These highlights, just like\nbookmarks now, could be private or public. When shared publicly,\nsomething interesting emerges: an overlay on existing content, a lens on\nthe written Web. In other words: *swarm highlights*.\n\nImagine a visual overlay of all public highlights, automatically shining\na light on what the swarm of readers found most useful, insightful,\nfunny, etc.\n\n![Swarm Highlights](https://dergigi.com/assets/images/bitcoin/2023-04-04-purple-text-orange-highlights/highlights.png)\n\nFurther, imagine the possibility of sharing these highlights as a \"type 1\" event\nwith one click, automatically tagging the highlighter(s)---as well as the\nauthor, of course---so that eventual sat-flows can be split and forwarded\nautomatically.\n\n![Automated value splits](https://dergigi.com/assets/images/bitcoin/2023-04-04-purple-text-orange-highlights/sat-flows.png)\n\nVoilà, you have a system that allows for value to flow back to those who\nprovide it, be it authors, editors, curators, or readers that willingly\nslog through the information jungle to share and highlight the best\nstuff (which is a form of curation, of course).\n\nZaps make nostr a defacto address book[^fn-pp] of payment information, which is\nto say lightning addresses, as of now. Thanks to [nostr wallet connect][nwc] (among\nother developments), sending sats ~~will soon be~~ is already as\nfrictionless as leaving a like.\n\n[^fn-pp]: The Yellow Pages are dead, long live [The Purple Pages](http://purplepag.es/)!\n\nValue-for-value and participatory payment flows are something that\ntraditional reading apps desperately lack, be it Pocket, Instapaper,\nReadwise, or the simple reading mode that is part of every browser.\n\nA neat side-effect of a more structured way to share passages of text is\nthat it enables semi-structured discussions around said\npassages---which could be another useful overlay inside\nspecial-purpose clients, providing context and further insights.[^5]\n\nFurther, imagine the option of seamlessly switching from text-on-screen\nto text-to-speech, allowing the user to stream sats if desired, as\nPodcasting 2.0 clients already do.[^3]\n\nImagine user-built curations of the best articles of the week, bundled\nneatly for your reading pleasure, incentivized by a small value split\nthat allows the curator to participate in the flow of sats.\n\nYou get the idea.\n\nI'm sure that the various implementation details will be hashed out,\nbut as I see it, 90% of the stuff is already there. Maybe we'll need\nanother NIP or two, but I don't see a reason why this can't be\nbuilt---and, more importantly: I don't see a reason why it wouldn't\nbe sustainable for everyone involved.\n\nMost puzzle pieces are already there, and the rest of them can probably\nbe implemented by custom event types. From the point of view of nostr,\nmost everything is an event: bookmarks are events, highlights are\nevents, marking something as read is an event, and sharing an excerpt or\na highlight is an event. Public actions are out in the open, private\nactions are encrypted, the data is not in a silo, and everyone wins.\nEspecially the users, those who are at the edge of the network and\nusually lose out on the value generated.\n\nIn this case, the reading case, the users are mostly \"consumers\" of\ncontent. What changes from the producing perspective, the perspective of\nthe writer?\n\n## Writing\n\nBack to the one thing that nostr got right: information is easy to\nspread but hard to stifle. In addition to that, digital information can\nbe copied perfectly, which is why it shouldn't matter where stuff is\npublished in the first place.\n\nAllow me to repeat this point in all caps, for emphasis: **IT SHOULD NOT\nMATTER WHERE INFORMATION IS PUBLISHED**, and, maybe even more\nimportantly, it shouldn't matter if it is published in a hundred\ndifferent places at once.[^fn-torrents]\n\nWhat matters is trust and accuracy, which is to say, digital signatures\nand reputation. To translate this to nostr speak: because every event is\nsigned by default, as long as you trust the person behind the signature,\nit doesn't matter from which relay the information is fetched.\n\nThis is already true (or mostly true) on the regular web. Whether you\nread the internet archive version of an article or the version that is\npublished by an online magazine, the version on the author's website,\nor the version read by some guy that has read more about Bitcoin than\nanyone else you know[^fn-guy]---it's all the same, essentially. What matters\nis the information itself.\n\n[^fn-guy]: There is only one such guy, as we all know, and it's this Guy: nostr:npub1h8nk2346qezka5cpm8jjh3yl5j88pf4ly2ptu7s6uu55wcfqy0wq36rpev\n\nPractically speaking, the source of truth in a hypernostrized world is---you\nguessed it---an event. An event signed by the author, which allows for\nthe information to be wrapped in a tamper-proof manner, which in turn\nallows the information to spread far and wide---without it being\nhosted in one place.\n\nThe first clients that focus on long-form content already exist, and I expect\nmore clients to pop up over time.[^4] As mentioned before, one could easily\nimagine [prism-like value splits][prism] seamlessly integrated into these\nclients, splitting zaps automatically to compensate writers, editors,\nproofreaders, and illustrators in a V4V fashion. Further, one could imagine\nvarious compute-intensive services built into these special-purpose clients,\nsuch as GPT Ghostwriters, or writing aids such as Grammarly and the like. All\nthese services could be seamlessly paid for in sats, without the requirement of\nany sign-ups or the gathering of any user data. That's the beauty of [money\nproper][rediscovery].\n\n![A clean and simple reading and writing interface](https://dergigi.com/assets/images/bitcoin/2023-04-04-purple-text-orange-highlights/nostr-reader-and-writer.png)\n\nPlagiarism is one issue that needs to be dealt with, of course. Humans\nare greedy, and some humans are assholes. Neither bitcoin nor nostr\nfixes this. However, while plagiarism detection is not necessarily\ntrivial, it is also not impossible, especially if most texts are\npublished on nostr first. Nostr-based publishing tools allow for\nOpenTimestamp attestations thanks\nto [NIP-03](https://github.com/nostr-protocol/nips/blob/master/03.md),\nwhich in turn allows for plagiarism detection based on \"first seen\"\nlookups.\n\nThat's just one way to deal with the problem, of course. In any case,\nI'm confident that we'll figure it out.\n\n## Value\n\nI believe that in the open ~~attention~~ information economy we find\nourselves in, value will mostly derive from effective curation,\ndissemination, and transmission of information, *not* the exclusive\nownership of it.\n\nAlthough it is still early days,\nthe [statistics](https://stats.podcastindex.org/v4v) around Podcasting\n2.0 and [nostr zaps](https://zaplife.lol/) clearly show that (a) people\nare willing to monetarily reward content they care about, and (b) the\nwillingness to send sats *increases* as friction *decreases*.\n\nThe ingenious thing about boostagrams and zaps is that they are direct\nand visible, which is to say, public and interactive. They are neither\nregular transactions nor simple donations---they are something else\nentirely. An unforgable value signal, a special form of gratitude and\nappreciation.\n\nContrast that with a link to Paypal or Patreon: impersonal, slow,\nindirect, and friction-laden. It's the opposite of a super-charged\ninteraction.\n\nWhile today's information jungle increasingly presents itself in the\nform of (short) videos and (long-form) audio, I believe that we will see\na renaissance of the written word, especially if we manage to move away\nfrom an economy built around attention, towards an economy built upon\nvalue and insight.\n\nThe orange future now has a purple hue, and I believe that it will be as\nbright as ever. We just have a lot of building to do.\n\n---\n\n## Further Reading\n\n- [A Vision for a Value-Enabled Web][vew]\n- [The Freedom of Value][busking]\n- [The Rediscovery of Money][prism]\n- [Lightning Prisms][rediscovery]\n\n[vew]: https://dergigi.com/vew\n[prism]: https://dergigi.com/prism\n[rediscovery]: https://dergigi.com/rediscovery\n[busking]: https://dergigi.com/busking\n\n## NIPs and Resources\n\n- [Nostr Resources][nr]\n- [value4value.info](https://value4value.info/)\n- [nips.be](https://nips.be/)\n- [NIP-23: Long-form content](https://github.com/nostr-protocol/nips/blob/master/23.md)\n- [NIP-57: Event-specific zap markers](https://github.com/nostr-protocol/nips/blob/master/57.md)\n- [NIP-47: Nostr Wallet Connect](https://github.com/getAlby/nips/blob/master/47.md)\n- [NIP-03: OpenTimestamps attestations for events](https://github.com/nostr-protocol/nips/blob/master/03.md)\n\nOriginally published on [dergigi.com](https://dergigi.com/reader)\n\n---\n\n[^1]: Paywalls work against this nature, which is why I consider them misguided at best and incredibly retarded at worst.\n\n[^2]: Fiat doesn't work for the [value-enabled web][vew], as fiat rails can never be open and permissionless. Digital fiat is never money. It is---and always will be---[credit][rediscovery].\n\n[^3]: Whether the recipient is a text-to-speech service provider or a human narrator doesn't even matter too much, sats will flow just the same.\n\n[^4]: [BlogStack](https://blogstack.io/) and [Habla](https://habla.news/) being two of them.\n\n[^5]: Use a URI as the discussion base (instead of a highlight), and you got yourself a [Disqus](https://disqus.com/) in purple feathers!\n\n[^fn-torrents]: That's what torrents got right, and [ipfs] for that matter.\n\n[nr]: https://nostr-resources.com\n[nwc]: https://nwc.getalby.com/\n[ipfs]: https://fiatjaf.com/d5031e5b.html\n", + "created_at": 1680614039, + "id": "9517aa60334990596fdea4493c1bde429c55e9cefb84d8d88a6758c4e3ebcabc", + "kind": 30023, + "pubkey": "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", + "sig": "c8c8c7ac8c288f6a6e1090e0e5f7eed85daaaba03eeab0a4936535f95ce066c08723bedcfb80a6af4941a9fafb9fe351950b9376d13ff0e81030cd4391840f04", + "tags": [ + ["d","1680612926599"], + ["title","Purple Text, Orange Highlights"], + ["summary","Using nostr as a base layer to create, consume, and monetize long-form content."], + ["published_at","1680613802"], + ["t","nostr"], + ["t","reading"], + ["t","writing"], + ["t","readability"], + ["t","highlights"], + ["t","pocket"], + ["t","instapaper"], + ["t","readwise"], + ["t","disqus"], + ["t","v4v"], + ["t","value4value"], + ["image","https://dergigi.com/assets/images/reader.jpg"], + ["p","b9e76546ba06456ed301d9e52bc49fa48e70a6bf2282be7a1ae72947612023dc"] + ] + } + """.trimIndent() + } @Test fun threadOrderTest() = @@ -139,17 +166,6 @@ class ThreadDualAxisChartAssemblerTest { null, ) - val keyPair = KeyPair() - val account = - Account( - settings = AccountSettings(keyPair = keyPair), - signer = NostrSignerInternal(keyPair), - scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), - ) - withContext(Dispatchers.Main) { - val user = account.userProfile().flow() - } - val filter = ThreadFeedFilter(account, naddr.toTag()) val calculatedFeed = filter.feed() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index bf885532f7..1129c77737 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -43,10 +43,10 @@ import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCacheFactory import com.vitorpamplona.amethyst.service.relayClient.CacheClientConnector import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector -import com.vitorpamplona.amethyst.service.relayClient.RelaySpeedLogger import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator +import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient @@ -118,7 +118,7 @@ class Amethyst : Application() { val notifyCoordinator = NotifyCoordinator(client) // Authenticates with relays. - val authCoordinator = AuthCoordinator(client) + val authCoordinator = AuthCoordinator(client, applicationIOScope) val logger = if (isDebug) RelaySpeedLogger(client) else null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 075fd5c45c..6fd951d95a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow -import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -49,13 +48,14 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent -import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent @@ -324,7 +324,7 @@ object LocalPreferences { ) putString( PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, - JsonMapper.mapper.writeValueAsString(settings.zapPaymentRequest?.denormalize()), + JsonMapper.mapper.writeValueAsString(settings.zapPaymentRequest.value?.denormalize()), ) if (settings.backupContactList != null) { putString( @@ -522,7 +522,6 @@ object LocalPreferences { "Unable to decode shared preferences: ${getString(PrefKeys.SHARED_SETTINGS, null)}", e, ) - e.printStackTrace() null } } @@ -619,7 +618,7 @@ object LocalPreferences { defaultStoriesFollowList = MutableStateFlow(defaultStoriesFollowList), defaultNotificationFollowList = MutableStateFlow(defaultNotificationFollowList), defaultDiscoveryFollowList = MutableStateFlow(defaultDiscoveryFollowList), - zapPaymentRequest = zapPaymentRequestServer?.normalize(), + zapPaymentRequest = MutableStateFlow(zapPaymentRequestServer?.normalize()), hideDeleteRequestDialog = hideDeleteRequestDialog, hideBlockAlertDialog = hideBlockAlertDialog, hideNIP17WarningDialog = hideNIP17WarningDialog, @@ -663,7 +662,6 @@ object LocalPreferences { } catch (e: Throwable) { if (e is CancellationException) throw e Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e) - e.printStackTrace() null } } @@ -678,7 +676,6 @@ object LocalPreferences { } catch (e: Throwable) { if (e is CancellationException) throw e Log.w("LocalPreferences", "Error Decoding $key from Preferences with value $value", e) - e.printStackTrace() null } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 7460795c9b..fa0a0b2a1f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -25,7 +25,9 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListDecryptionCache import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState @@ -38,19 +40,30 @@ import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState import com.vitorpamplona.amethyst.model.nip18Reposts.RepostAction import com.vitorpamplona.amethyst.model.nip25Reactions.ReactionAction +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListDecryptionCache import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListState import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction -import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState -import com.vitorpamplona.amethyst.model.nip51Lists.BlockPeopleListState -import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState -import com.vitorpamplona.amethyst.model.nip51Lists.GeohashListState -import com.vitorpamplona.amethyst.model.nip51Lists.HashtagListState +import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState +import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState -import com.vitorpamplona.amethyst.model.nip51Lists.MuteListState -import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleListState +import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.PeopleListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState +import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState +import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState +import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState +import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState @@ -59,13 +72,13 @@ import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState import com.vitorpamplona.amethyst.model.serverList.MergedServerListState import com.vitorpamplona.amethyst.model.serverList.TrustedRelayListsState +import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches import com.vitorpamplona.amethyst.model.topNavFeeds.FeedTopNavFilterState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState import com.vitorpamplona.amethyst.model.torState.TorRelayState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent @@ -93,7 +106,6 @@ import com.vitorpamplona.quartz.experimental.profileGallery.mimeType import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle @@ -108,10 +120,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal -import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNote import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses -import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes @@ -123,6 +132,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver +import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip04Dm.messages.reply import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent @@ -140,33 +150,32 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.Entity import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip30CustomEmoji.emojis -import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip37Drafts.DraftBuilder import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent -import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent -import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.Response -import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapCache import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -190,26 +199,19 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent -import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.math.BigDecimal import java.util.Base64 import java.util.Locale -import kotlin.coroutines.resume @OptIn(DelicateCoroutinesApi::class) @Stable @@ -227,25 +229,51 @@ class Account( val userMetadata = UserMetadataState(signer, cache, scope, settings) + val nip47SignerState = NwcSignerState(signer, cache, scope, settings) + val nip65RelayList = Nip65RelayListState(signer, cache, scope, settings) - val dmRelayList = DmRelayListState(signer, cache, scope, settings) - val searchRelayList = SearchRelayListState(signer, cache, scope, settings) - val privateStorageRelayList = PrivateStorageRelayListState(signer, cache, scope, settings) val localRelayList = LocalRelayListState(signer, cache, scope, settings) - val trustedRelayList = TrustedRelayListState(signer, cache, scope, settings) - val blockedRelayList = BlockedRelayListState(signer, cache, scope, settings) + + val dmRelayList = DmRelayListState(signer, cache, scope, settings) + + val privateStorageDecryptionCache = PrivateStorageRelayListDecryptionCache(signer) + val privateStorageRelayList = PrivateStorageRelayListState(signer, cache, privateStorageDecryptionCache, scope, settings) + + val searchRelayListDecryptionCache = SearchRelayListDecryptionCache(signer) + val searchRelayList = SearchRelayListState(signer, cache, searchRelayListDecryptionCache, scope, settings) + + val trustedRelayListDecryptionCache = TrustedRelayListDecryptionCache(signer) + val trustedRelayList = TrustedRelayListState(signer, cache, trustedRelayListDecryptionCache, scope, settings) + + val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer) + val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings) val kind3FollowList = FollowListState(signer, cache, scope, settings) - val ephemeralChatList = EphemeralChatListState(signer, cache, scope, settings) - val publicChatList = PublicChatListState(signer, cache, scope, settings) - val communityList = CommunityListState(signer, cache, scope, settings) - val hashtagList = HashtagListState(signer, cache, scope, settings) - val geohashList = GeohashListState(signer, cache, scope, settings) - val muteList = MuteListState(signer, cache, scope, settings) - val blockPeopleList = BlockPeopleListState(signer, cache, scope) + val ephemeralChatListDecryptionCache = EphemeralChatListDecryptionCache(signer) + val ephemeralChatList = EphemeralChatListState(signer, cache, ephemeralChatListDecryptionCache, scope, settings) + + val publicChatListDecryptionCache = PublicChatListDecryptionCache(signer) + val publicChatList = PublicChatListState(signer, cache, publicChatListDecryptionCache, scope, settings) + + val communityListDecryptionCache = CommunityListDecryptionCache(signer) + val communityList = CommunityListState(signer, cache, communityListDecryptionCache, scope, settings) + + val hashtagListDecryptionCache = HashtagListDecryptionCache(signer) + val hashtagList = HashtagListState(signer, cache, hashtagListDecryptionCache, scope, settings) + + val geohashListDecryptionCache = GeohashListDecryptionCache(signer) + val geohashList = GeohashListState(signer, cache, geohashListDecryptionCache, scope, settings) + + val muteListDecryptionCache = MuteListDecryptionCache(signer) + val muteList = MuteListState(signer, cache, muteListDecryptionCache, scope, settings) + + val peopleListDecryptionCache = PeopleListDecryptionCache(signer) + val blockPeopleList = BlockPeopleListState(signer, cache, peopleListDecryptionCache, scope) + val hiddenUsers = HiddenUsersState(muteList.flow, blockPeopleList.flow, scope, settings) + val bookmarkState = BookmarkListState(signer, cache, scope) val emoji = EmojiPackState(signer, cache, scope) val appSpecific = AppSpecificState(signer, cache, scope, settings) @@ -271,6 +299,21 @@ class Account( // Merges all follow lists to create a single All Follows feed. val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope) + val privateDMDecryptionCache = PrivateDMCache(signer) + val privateZapsDecryptionCache = PrivateZapCache(signer) + val draftsDecryptionCache = DraftEventCache(signer) + + val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) + + val feedDecryptionCaches = + FeedDecryptionCaches( + peopleListCache = peopleListDecryptionCache, + muteListCache = muteListDecryptionCache, + communityListCache = communityListDecryptionCache, + hashtagCache = hashtagListDecryptionCache, + geohashCache = geohashListDecryptionCache, + ) + // App-ready Feeds val liveHomeFollowLists: StateFlow = FeedTopNavFilterState( @@ -279,6 +322,7 @@ class Account( locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, blockedRelays = blockedRelayList.flow, + caches = feedDecryptionCaches, signer = signer, scope = scope, ).flow @@ -292,6 +336,7 @@ class Account( locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, blockedRelays = blockedRelayList.flow, + caches = feedDecryptionCaches, signer = signer, scope = scope, ).flow @@ -305,6 +350,7 @@ class Account( locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, blockedRelays = blockedRelayList.flow, + caches = feedDecryptionCaches, signer = signer, scope = scope, ).flow @@ -318,6 +364,7 @@ class Account( locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, blockedRelays = blockedRelayList.flow, + caches = feedDecryptionCaches, signer = signer, scope = scope, ).flow @@ -338,36 +385,9 @@ class Account( val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope) - fun decryptPeopleList( - event: GeneralListEvent, - onReady: (Array>) -> Unit, - ) = event.privateTags(signer, onReady) - - @OptIn(FlowPreview::class) - val decryptBookmarks: Flow by lazy { - userProfile() - .flow() - .bookmarks.stateFlow - .map { userState -> - if (userState.user.latestBookmarkList == null) { - null - } else { - val result = - tryAndWait { continuation -> - userState.user.latestBookmarkList?.privateTags(signer) { - continuation.resume(userState.user.latestBookmarkList) - } - } - - result - } - }.debounce(1000) - .flowOn(Dispatchers.Default) - } - fun isWriteable(): Boolean = settings.isWriteable() - fun updateWarnReports(warnReports: Boolean): Boolean { + suspend fun updateWarnReports(warnReports: Boolean): Boolean { if (settings.updateWarnReports(warnReports)) { sendNewAppSpecificData() return true @@ -375,7 +395,7 @@ class Account( return false } - fun updateFilterSpam(filterSpam: Boolean): Boolean { + suspend fun updateFilterSpam(filterSpam: Boolean): Boolean { if (settings.updateFilterSpam(filterSpam)) { if (!settings.syncedSettings.security.filterSpamFromStrangers.value) { hiddenUsers.resetTransientUsers() @@ -387,19 +407,19 @@ class Account( return false } - fun updateShowSensitiveContent(show: Boolean?) { + suspend fun updateShowSensitiveContent(show: Boolean?) { if (settings.updateShowSensitiveContent(show)) { sendNewAppSpecificData() } } - fun changeReactionTypes(reactionSet: List) { + suspend fun changeReactionTypes(reactionSet: List) { if (settings.changeReactionTypes(reactionSet)) { sendNewAppSpecificData() } } - fun updateZapAmounts( + suspend fun updateZapAmounts( amountSet: List, selectedZapType: LnZapEvent.ZapType, nip47Update: Nip47WalletConnect.Nip47URINorm?, @@ -415,18 +435,18 @@ class Account( } } - fun toggleDontTranslateFrom(languageCode: String) { + suspend fun toggleDontTranslateFrom(languageCode: String) { settings.toggleDontTranslateFrom(languageCode) sendNewAppSpecificData() } - fun updateTranslateTo(languageCode: Locale) { + suspend fun updateTranslateTo(languageCode: Locale) { if (settings.updateTranslateTo(languageCode)) { sendNewAppSpecificData() } } - fun prefer( + suspend fun prefer( source: String, target: String, preference: String, @@ -435,17 +455,13 @@ class Account( sendNewAppSpecificData() } - private fun sendNewAppSpecificData() { - if (!isWriteable()) return - - appSpecific.saveNewAppSpecificData(::sendMyPublicAndPrivateOutbox) - } + private suspend fun sendNewAppSpecificData() = sendMyPublicAndPrivateOutbox(appSpecific.saveNewAppSpecificData()) suspend fun countFollowersOf(pubkey: HexKey): Int = cache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkey) ?: false } suspend fun followerCount(): Int = countFollowersOf(signer.pubKey) - fun sendNewUserMetadata( + suspend fun sendNewUserMetadata( name: String? = null, picture: String? = null, banner: String? = null, @@ -461,21 +477,23 @@ class Account( ) { if (!isWriteable()) return - userMetadata.sendNewUserMetadata( - name, - picture, - banner, - website, - pronouns, - about, - nip05, - lnAddress, - lnURL, - twitter, - mastodon, - github, - ::sendMyPublicAndPrivateOutbox, - ) + val userMetadataEvent = + userMetadata.sendNewUserMetadata( + name, + picture, + banner, + website, + pronouns, + about, + nip05, + lnAddress, + lnURL, + twitter, + mastodon, + github, + ) + + sendEverywhere(userMetadataEvent) } fun reactionTo( @@ -507,54 +525,24 @@ class Account( onPrivate = ::broadcastPrivately, ) - fun createZapRequestFor( - note: Note, + suspend fun createZapRequestFor( + event: Event, pollOption: Int?, message: String = "", zapType: LnZapEvent.ZapType, toUser: User?, additionalRelays: Set? = null, - onReady: (LnZapRequestEvent) -> Unit, - ) { - if (!isWriteable()) return - + ): LnZapRequestEvent { val relays = nip65RelayList.inboxFlow.value + (additionalRelays ?: emptySet()) - - note.event?.let { event -> - LnZapRequestEvent.create( - event, - relays = relays.mapTo(mutableSetOf()) { it.url }, - signer, - pollOption, - message, - zapType, - toUser?.pubkeyHex, - onReady = onReady, - ) - } - } - - fun hasWalletConnectSetup(): Boolean = settings.zapPaymentRequest != null - - fun isNIP47Author(pubkeyHex: String?): Boolean = (getNIP47Signer().pubKey == pubkeyHex) - - fun getNIP47Signer(): NostrSigner = - settings.zapPaymentRequest - ?.secret - ?.hexToByteArray() - ?.let { NostrSignerInternal(KeyPair(it)) } - ?: signer - - fun decryptZapPaymentResponseEvent( - zapResponseEvent: LnZapPaymentResponseEvent, - onReady: (Response) -> Unit, - ) { - val myNip47 = settings.zapPaymentRequest ?: return - - val signer = - myNip47.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) } ?: signer - - zapResponseEvent.response(signer, onReady) + return LnZapRequestEvent.create( + event, + relays = relays.mapTo(mutableSetOf()) { it.url }, + signer, + pollOption, + message, + zapType, + toUser?.pubkeyHex, + ) } suspend fun calculateIfNoteWasZappedByAccount( @@ -564,141 +552,92 @@ class Account( zappedNote?.isZappedBy(userProfile(), this, onWasZapped) } - suspend fun calculateZappedAmount( - zappedNote: Note?, - onReady: (BigDecimal) -> Unit, - ) { - zappedNote?.zappedAmountWithNWCPayments(getNIP47Signer(), onReady) - } + suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(nip47SignerState) - fun sendZapPaymentRequestFor( + suspend fun sendZapPaymentRequestFor( bolt11: String, zappedNote: Note?, - onSent: () -> Unit, onResponse: (Response?) -> Unit, ) { - if (!isWriteable()) return - - settings.zapPaymentRequest?.let { nip47 -> - val signer = - nip47.secret?.hexToByteArray()?.let { NostrSignerInternal(KeyPair(it)) } ?: signer - - LnZapPaymentRequestEvent.create(bolt11, nip47.pubKeyHex, signer) { event -> - val filter = - NWCPaymentQueryState( - fromServiceHex = nip47.pubKeyHex, - toUserHex = event.pubKey, - replyingToHex = event.id, - relay = nip47.relayUri, - ) - - Amethyst.instance.sources.nwc - .subscribe(filter) - - GlobalScope.launch(Dispatchers.IO) { - delay(60000) // waits 1 minute to complete payment. - Amethyst.instance.sources.nwc - .unsubscribe(filter) - } - - cache.consume(event, zappedNote, true, nip47.relayUri) { - it.response(signer) { onResponse(it) } - } - - client.send(event, setOf(nip47.relayUri)) - - onSent() - } - } + val (event, relay) = nip47SignerState.sendZapPaymentRequestFor(bolt11, zappedNote, onResponse) + client.send(event, setOf(relay)) } - fun createZapRequestFor( + suspend fun createZapRequestFor( user: User, message: String = "", zapType: LnZapEvent.ZapType, - onReady: (LnZapRequestEvent) -> Unit, - ) { + ): LnZapRequestEvent { val relays = nip65RelayList.inboxFlow.value + user.inboxRelays() - LnZapRequestEvent.create( - userHex = user.pubkeyHex, - relays = relays, - signer = signer, - message = message, - zapType = zapType, - ) { zapRequest -> - LocalCache.justConsumeMyOwnEvent(zapRequest) - onReady(zapRequest) - } + val zapRequest = + LnZapRequestEvent.create( + userHex = user.pubkeyHex, + relays = relays, + signer = signer, + message = message, + zapType = zapType, + ) + + cache.justConsumeMyOwnEvent(zapRequest) + return zapRequest } suspend fun report( note: Note, type: ReportType, content: String = "", - ) = ReportAction.report(note, type, content, userProfile(), signer, ::sendMyPublicAndPrivateOutbox) + ) = sendMyPublicAndPrivateOutbox(ReportAction.report(note, type, content, userProfile(), signer)) suspend fun report( user: User, type: ReportType, - ) = ReportAction.report(user, type, userProfile(), signer, ::sendMyPublicAndPrivateOutbox) + ) = sendMyPublicAndPrivateOutbox(ReportAction.report(user, type, userProfile(), signer)) - fun delete(note: Note) { + suspend fun delete(note: Note) { delete(listOf(note)) } - fun delete(notes: List) { + suspend fun delete(notes: List) { if (!isWriteable()) return val myNotes = notes.filter { it.author == userProfile() && it.event != null } if (myNotes.isNotEmpty()) { // chunks in 200 elements to avoid going over the 65KB limit for events. myNotes.chunked(200).forEach { chunkedList -> - signer.sign( - DeletionEvent.build(chunkedList.mapNotNull { it.event }), - ) { deletionEvent -> - val myRelayList = outboxRelays.flow.value.toMutableSet() - chunkedList.forEach { - myRelayList.addAll(it.relays) - } - - client.send(deletionEvent, myRelayList) - cache.justConsumeMyOwnEvent(deletionEvent) + val template = DeletionEvent.build(chunkedList.mapNotNull { it.event }) + val deletionEvent = signer.sign(template) + val myRelayList = outboxRelays.flow.value.toMutableSet() + chunkedList.forEach { + myRelayList.addAll(it.relays) } + + client.send(deletionEvent, myRelayList) + cache.justConsumeMyOwnEvent(deletionEvent) } } } - fun delete( + suspend fun delete( event: Event, additionalRelays: Set, ) { if (!isWriteable()) return if (event.pubKey != signer.pubKey) return - signer.sign( - DeletionEvent.build(listOf(event)), - ) { deletionEvent -> - client.send(deletionEvent, outboxRelays.flow.value + additionalRelays) - cache.justConsumeMyOwnEvent(deletionEvent) - } + val deletionEvent = + signer.sign( + DeletionEvent.build(listOf(event)), + ) + client.send(deletionEvent, outboxRelays.flow.value + additionalRelays) + cache.justConsumeMyOwnEvent(deletionEvent) } suspend fun createHTTPAuthorization( url: String, method: String, body: ByteArray? = null, - ): HTTPAuthorizationEvent? { - if (!isWriteable()) return null - - val template = HTTPAuthorizationEvent.build(url, method, body) - - return tryAndWait { continuation -> - signer.sign(template) { - continuation.resume(it) - } - } - } + ): HTTPAuthorizationEvent? = signer.sign(HTTPAuthorizationEvent.build(url, method, body)) suspend fun createBlossomUploadAuth( hash: HexKey, @@ -712,9 +651,9 @@ class Account( ) = blossomServers.createBlossomDeleteAuth(hash, alt) suspend fun boost(note: Note) { - RepostAction.repost(note, signer) { - client.send(it, computeMyReactionToNote(note, it)) - cache.justConsumeMyOwnEvent(it) + RepostAction.repost(note, signer)?.let { event -> + client.send(event, computeMyReactionToNote(note, event)) + cache.justConsumeMyOwnEvent(event) } } @@ -738,7 +677,7 @@ class Account( if (pubkey == userProfile().pubkeyHex) { notificationRelays.flow.value } else { - LocalCache + cache .getUserIfExists(pubkey) ?.inboxRelays() ?.ifEmpty { null } @@ -747,7 +686,7 @@ class Account( } } - val channelRelays = LocalCache.getAnyChannel(note)?.relays() ?: emptySet() + val channelRelays = cache.getAnyChannel(note)?.relays() ?: emptySet() val replyRelays = note.replyTo?.flatMapTo(mutableSetOf()) { @@ -794,7 +733,7 @@ class Account( if (pubkey == userProfile().pubkeyHex) { notificationRelays.flow.value } else { - LocalCache + cache .getUserIfExists(pubkey) ?.inboxRelays() ?.ifEmpty { null } @@ -802,7 +741,7 @@ class Account( ?: cache.relayHints.hintsForKey(pubkey).toSet() } - private fun computeRelaysForChannels(event: Event): Set = LocalCache.getAnyChannel(event)?.relays() ?: emptySet() + private fun computeRelaysForChannels(event: Event): Set = cache.getAnyChannel(event)?.relays() ?: emptySet() fun computeRelayListToBroadcast(event: Event): Set { if (event is MetadataEvent || event is AdvertisedRelayListEvent) { @@ -943,27 +882,24 @@ class Account( val otsResolver = otsResolver() - settings.pendingAttestations.value.forEach { pair -> - val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(pair.value), pair.key, otsResolver) + settings.pendingAttestations.value.toList().forEach { (key, value) -> + val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver) if (otsState != null) { - val hint = cache.getNoteIfExists(pair.key)?.toEventHint() - + val hint = cache.getNoteIfExists(key)?.toEventHint() val template = if (hint != null) { OtsEvent.build(hint, otsState) } else { - OtsEvent.build(pair.key, otsState) + OtsEvent.build(key, otsState) } - signer.sign(template) { - cache.justConsumeMyOwnEvent(it) - client.send(it, computeRelayListToBroadcast(it)) + val otsEvent = signer.sign(template) - settings.pendingAttestations.update { - it - pair.key - } - } + cache.justConsumeMyOwnEvent(otsEvent) + client.send(otsEvent, computeRelayListToBroadcast(otsEvent)) + + settings.pendingAttestations.update { it - key } } } } @@ -983,63 +919,70 @@ class Account( settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id, otsResolver))) } - fun follow(user: User) = kind3FollowList.follow(user, this::sendMyPublicAndPrivateOutbox) + suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user)) - fun unfollow(user: User) = kind3FollowList.unfollow(user, this::sendMyPublicAndPrivateOutbox) + suspend fun unfollow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.unfollow(user)) - fun follow(channel: PublicChatChannel) = publicChatList.follow(channel, ::sendToPrivateOutboxAndLocal) + suspend fun follow(channel: PublicChatChannel) = sendMyPublicAndPrivateOutbox(publicChatList.follow(channel)) - fun unfollow(channel: PublicChatChannel) = publicChatList.unfollow(channel, ::sendToPrivateOutboxAndLocal) + suspend fun unfollow(channel: PublicChatChannel) = sendMyPublicAndPrivateOutbox(publicChatList.unfollow(channel)) - fun follow(channel: EphemeralChatChannel) = ephemeralChatList.follow(channel, ::sendToPrivateOutboxAndLocal) + suspend fun follow(channel: EphemeralChatChannel) = sendMyPublicAndPrivateOutbox(ephemeralChatList.follow(channel)) - fun unfollow(channel: EphemeralChatChannel) = ephemeralChatList.unfollow(channel, ::sendToPrivateOutboxAndLocal) + suspend fun unfollow(channel: EphemeralChatChannel) = sendMyPublicAndPrivateOutbox(ephemeralChatList.unfollow(channel)) - fun follow(community: AddressableNote) = communityList.follow(community, ::sendToPrivateOutboxAndLocal) + suspend fun follow(community: AddressableNote) = sendMyPublicAndPrivateOutbox(communityList.follow(community)) - fun unfollow(community: AddressableNote) = communityList.unfollow(community, ::sendToPrivateOutboxAndLocal) + suspend fun unfollow(community: AddressableNote) = sendMyPublicAndPrivateOutbox(communityList.unfollow(community)) - fun followHashtag(tag: String) = hashtagList.follow(tag, ::sendMyPublicAndPrivateOutbox) + suspend fun followHashtag(tag: String) = sendMyPublicAndPrivateOutbox(hashtagList.follow(tag)) - fun unfollowHashtag(tag: String) = hashtagList.unfollow(tag, ::sendMyPublicAndPrivateOutbox) + suspend fun unfollowHashtag(tag: String) = sendMyPublicAndPrivateOutbox(hashtagList.unfollow(tag)) - fun followGeohash(geohash: String) = geohashList.follow(geohash, ::sendMyPublicAndPrivateOutbox) + suspend fun followGeohash(geohash: String) = sendMyPublicAndPrivateOutbox(geohashList.follow(geohash)) - fun unfollowGeohash(geohash: String) = geohashList.unfollow(geohash, ::sendMyPublicAndPrivateOutbox) + suspend fun unfollowGeohash(geohash: String) = sendMyPublicAndPrivateOutbox(geohashList.unfollow(geohash)) - fun sendMyPublicAndPrivateOutbox(event: Event) { + fun sendMyPublicAndPrivateOutbox(event: Event?) { + if (event == null) return client.send(event, outboxRelays.flow.value) cache.justConsumeMyOwnEvent(event) } - fun createNip95( + fun sendMyPublicAndPrivateOutbox(events: List) { + events.forEach { + client.send(it, outboxRelays.flow.value) + cache.justConsumeMyOwnEvent(it) + } + } + + fun sendEverywhere(event: Event) { + client.send(event, outboxRelays.flow.value + client.relayStatusFlow().value.available) + cache.justConsumeMyOwnEvent(event) + } + + suspend fun createNip95( byteArray: ByteArray, headerInfo: FileHeader, alt: String?, contentWarningReason: String?, - onReady: (Pair) -> Unit, - ) { - if (!isWriteable()) return + ): Pair { + val data = signer.sign(FileStorageEvent.build(byteArray, headerInfo.mimeType)) - signer.sign(FileStorageEvent.build(byteArray, headerInfo.mimeType)) { data -> - val template = - FileStorageHeaderEvent.build(EventHintBundle(data, userProfile().bestRelayHint()), alt) { - hash(headerInfo.hash) - fileSize(headerInfo.size) + val template = + FileStorageHeaderEvent.build(EventHintBundle(data, userProfile().bestRelayHint()), alt) { + hash(headerInfo.hash) + fileSize(headerInfo.size) - headerInfo.mimeType?.let { mimeType(it) } - headerInfo.dim?.let { dimension(it) } - headerInfo.blurHash?.let { blurhash(it.blurhash) } + headerInfo.mimeType?.let { mimeType(it) } + headerInfo.dim?.let { dimension(it) } + headerInfo.blurHash?.let { blurhash(it.blurhash) } - contentWarningReason?.let { contentWarning(contentWarningReason) } - } - - signer.sign(template) { signedEvent -> - onReady( - Pair(data, signedEvent), - ) + contentWarningReason?.let { contentWarning(contentWarningReason) } } - } + + val signedEvent = signer.sign(template) + return Pair(data, signedEvent) } fun consumeAndSendNip95( @@ -1089,40 +1032,10 @@ class Account( cache.getNoteIfExists(signedEvent.id)?.let { onReady(it) } } - fun createHeader( - imageUrl: String, - magnetUri: String?, - headerInfo: FileHeader, - alt: String?, - contentWarningReason: String? = null, - originalHash: String? = null, - onReady: (FileHeaderEvent) -> Unit, - ) { - if (!isWriteable()) return - - signer.sign( - FileHeaderEvent.build(imageUrl, alt) { - hash(headerInfo.hash) - fileSize(headerInfo.size) - - headerInfo.mimeType?.let { mimeType(it) } - headerInfo.dim?.let { dimension(it) } - headerInfo.blurHash?.let { blurhash(it.blurhash) } - - originalHash?.let { originalHash(it) } - magnetUri?.let { magnet(it) } - - contentWarningReason?.let { contentWarning(contentWarningReason) } - }, - onReady, - ) - } - - fun sendAllAsOnePictureEvent( + suspend fun sendAllAsOnePictureEvent( urlHeaderInfo: Map, caption: String?, contentWarningReason: String?, - onReady: (Note) -> Unit, ) { val iMetas = urlHeaderInfo.map { @@ -1154,19 +1067,16 @@ class Account( contentWarningReason?.let { contentWarning(contentWarningReason) } } - signAndComputeBroadcast(template) { event -> - cache.getNoteIfExists(event.id)?.let { onReady(it) } - } + signAndComputeBroadcast(template) } - fun sendHeader( + suspend fun sendHeader( url: String, magnetUri: String?, headerInfo: FileHeader, alt: String?, contentWarningReason: String?, originalHash: String? = null, - onReady: (Note) -> Unit, ) { if (!isWriteable()) return @@ -1233,68 +1143,81 @@ class Account( } } - signAndComputeBroadcast(template) { event -> - cache.getNoteIfExists(event.id)?.let { onReady(it) } - } + signAndComputeBroadcast(template) } - fun signAndSendPrivately( + suspend fun signAndSendPrivately( template: EventTemplate, relayList: Set, ) { - signer.sign(template) { - cache.justConsumeMyOwnEvent(it) - client.send(it, relayList) - } + val event = signer.sign(template) + cache.justConsumeMyOwnEvent(event) + client.send(event, relayList) } - fun signAndSendPrivatelyOrBroadcast( + suspend fun signAndSendPrivatelyOrBroadcast( template: EventTemplate, relayList: (T) -> List?, - onDone: (T) -> Unit = {}, - ) { - signer.sign(template) { - cache.justConsumeMyOwnEvent(it) - val relays = relayList(it) - if (relays != null && relays.isNotEmpty()) { - client.send(it, relays.toSet()) - } else { - client.send(it, computeRelayListToBroadcast(it)) - } - onDone(it) + ): T { + val event = signer.sign(template) + cache.justConsumeMyOwnEvent(event) + val relays = relayList(event) + if (relays != null && relays.isNotEmpty()) { + client.send(event, relays.toSet()) + } else { + client.send(event, computeRelayListToBroadcast(event)) } + return event } - fun signAndComputeBroadcast( + suspend fun signAndComputeBroadcast( template: EventTemplate, broadcast: List = emptyList(), - onDone: (T) -> Unit = {}, - ) { - signer.sign(template) { event -> - cache.justConsumeMyOwnEvent(event) - val note = - if (event is AddressableEvent) { - cache.getOrCreateAddressableNote(event.address()) - } else { - cache.getOrCreateNote(event.id) - } + ): T { + val event = signer.sign(template) + cache.justConsumeMyOwnEvent(event) + val note = + if (event is AddressableEvent) { + cache.getOrCreateAddressableNote(event.address()) + } else { + cache.getOrCreateNote(event.id) + } - val relayList = computeRelayListToBroadcast(note) + val relayList = computeRelayListToBroadcast(note) - client.send(event, relayList) + client.send(event, relayList) - broadcast.forEach { client.send(it, relayList) } + broadcast.forEach { client.send(it, relayList) } - onDone(event) - } + return event } - fun signAndSend( + suspend fun signAndSend( + draftTag: String?, + template: EventTemplate, + relayList: Set, + broadcastNotes: List, + ) = signAndSend(draftTag, template, relayList, mapEntitiesToNotes(broadcastNotes).toSet()) + + suspend fun signAndSend( + draftTag: String?, template: EventTemplate, relayList: Set, broadcastNotes: Set, ) { - signer.sign(template) { + if (draftTag != null) { + val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) + val draftEvent = + DraftEvent.create( + dTag = draftTag, + innerEvent = rumor, + anchorTagArray = emptyList(), + signer = signer, + ) + draftsDecryptionCache.preload(draftEvent, rumor) + sendDraftEvent(draftEvent) + } else { + val it = signer.sign(template) cache.justConsumeMyOwnEvent(it) client.send(it, relayList = relayList) @@ -1302,36 +1225,7 @@ class Account( } } - fun signAndSend( - draftTag: String?, - template: EventTemplate, - relayList: Set, - broadcastNotes: List, - ) = signAndSend(draftTag, template, relayList, mapEntitiesToNotes(broadcastNotes).toSet()) - - fun signAndSend( - draftTag: String?, - template: EventTemplate, - relayList: Set, - broadcastNotes: Set, - ) { - if (draftTag != null) { - signer.assembleRumor(template) { rumor -> - DraftEvent.create(draftTag, rumor, emptyList(), signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } - } else { - signer.sign(template) { - cache.justConsumeMyOwnEvent(it) - client.send(it, relayList = relayList) - - broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relayList) } } - } - } - } - - fun signAndSendNIP04Message( + suspend fun signAndSendNIP04Message( draftTag: String?, template: EventTemplate, relayList: Set, @@ -1340,76 +1234,61 @@ class Account( if (template.content.isEmpty()) { deleteDraft(draftTag) } else { - signer.assembleRumor(template) { rumor -> - DraftEvent.create(draftTag, rumor, emptyList(), signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } + val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) + val draftEvent = DraftEvent.create(draftTag, rumor, emptyList(), signer) + draftsDecryptionCache.preload(draftEvent, rumor) + sendDraftEvent(draftEvent) } } else { - signer.sign(template) { - cache.justConsumeMyOwnEvent(it) - client.send(it, relayList) - } + val newEvent = signer.sign(template) + cache.justConsumeMyOwnEvent(newEvent) + client.send(newEvent, relayList) } } - fun signAndSendWithList( + suspend fun signAndSendWithList( draftTag: String?, template: EventTemplate, relayList: Collection, broadcastNotes: Set, ) { if (draftTag != null) { - signer.assembleRumor(template) { rumor -> - DraftEvent.create(draftTag, rumor, emptyList(), signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } + val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) + val draftEvent = DraftEvent.create(draftTag, rumor, emptyList(), signer) + draftsDecryptionCache.preload(draftEvent, rumor) + sendDraftEvent(draftEvent) } else { - signer.sign(template) { - cache.justConsumeMyOwnEvent(it) + val event = signer.sign(template) + cache.justConsumeMyOwnEvent(event) - val relaySet = relayList.toSet() + val relaySet = relayList.toSet() - client.send(it, relayList = relaySet) - broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relaySet) } } - } + client.send(event, relayList = relaySet) + broadcastNotes.forEach { it.event?.let { client.send(it, relayList = relaySet) } } } } - fun sendTorrentComment( - draftTag: String?, - template: EventTemplate, - broadcastNotes: Set, - relayList: Set, - ) { - if (!isWriteable()) return - - signAndSend(draftTag, template, relayList, broadcastNotes) - } - - fun createAndSendDraft( + suspend fun createAndSendDraft( draftTag: String, template: EventTemplate, ) { - val rumor = signer.assembleRumor(template) - DraftBuilder.encryptAndSign(draftTag, rumor, signer) { draftEvent -> - sendDraftEvent(draftEvent) - } + val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) + val draftEvent = DraftBuilder.encryptAndSign(draftTag, rumor, signer) + draftsDecryptionCache.preload(draftEvent, rumor) + sendDraftEvent(draftEvent) } - fun deleteDraft(draftTag: String) { + suspend fun deleteDraft(draftTag: String) { val key = DraftEvent.createAddressTag(userProfile().pubkeyHex, draftTag) cache.getAddressableNoteIfExists(key)?.let { note -> val noteEvent = note.event if (noteEvent is DraftEvent) { - noteEvent.createDeletedEvent(signer) { - client.send(it, outboxRelays.flow.value + note.relays) - cache.justConsumeMyOwnEvent(it) + val deletedDraftEvent = noteEvent.createDeletedEvent(signer) + client.send(deletedDraftEvent, outboxRelays.flow.value + note.relays) + cache.justConsumeMyOwnEvent(deletedDraftEvent) - delete(it, note.relays.toSet()) - } + delete(deletedDraftEvent, note.relays.toSet()) + draftsDecryptionCache.delete(noteEvent) } } } @@ -1430,7 +1309,7 @@ class Account( currentSceneRelay = readingSceneRelay, ) - signer.sign(template, ::sendToPrivateOutboxAndLocal) + sendToPrivateOutboxAndLocal(signer.sign(template)) } suspend fun updateInteractiveStoryReadingState( @@ -1447,7 +1326,7 @@ class Account( currentSceneRelay = readingSceneRelay, ) - signer.sign(template, ::sendToPrivateOutboxAndLocal) + sendToPrivateOutboxAndLocal(signer.sign(template)) } fun mapEntitiesToNotes(entities: List): List = @@ -1455,7 +1334,7 @@ class Account( when (it) { is NPub -> null is NProfile -> null - is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> cache.getOrCreateNote(it.hex) + is NNote -> cache.getOrCreateNote(it.hex) is NEvent -> cache.getOrCreateNote(it.hex) is NEmbed -> cache.getOrCreateNote(it.event.id) is NAddress -> cache.checkGetOrCreateAddressableNote(it.aTag()) @@ -1561,7 +1440,7 @@ class Account( signAndSendWithList(draftTag, template, relays, setOf(bounty)) } - fun sendEdit( + suspend fun sendEdit( message: String, originalNote: Note, notify: HexKey?, @@ -1572,24 +1451,25 @@ class Account( val idHex = originalNote.event?.id ?: return - TextNoteModificationEvent.create( - content = message, - eventId = idHex, - notify = notify, - summary = summary, - signer = signer, - ) { event -> - cache.justConsumeMyOwnEvent(event) - val note = cache.getOrCreateNote(event.id) - val relayList = computeRelayListToBroadcast(note) + val event = + TextNoteModificationEvent.create( + content = message, + eventId = idHex, + notify = notify, + summary = summary, + signer = signer, + ) - client.send(event, relayList = relayList) + cache.justConsumeMyOwnEvent(event) + val note = cache.getOrCreateNote(event.id) + val relayList = computeRelayListToBroadcast(note) - broadcast.forEach { client.send(it, relayList) } - } + client.send(event, relayList = relayList) + + broadcast.forEach { client.send(it, relayList) } } - fun sendPrivateMessage( + suspend fun sendPrivateMessage( message: String, toUser: PTag, replyingTo: Note? = null, @@ -1603,36 +1483,36 @@ class Account( ) { if (!isWriteable()) return - signer.nip04Encrypt( - PrivateDmEvent.prepareMessageToEncrypt(message, imetas), - toUser.pubKey, - ) { encryptedContent -> - val template = - PrivateDmEvent.build(toUser, encryptedContent) { - replyingTo?.let { reply(it.toEId()) } + val encryptedContent = + signer.nip04Encrypt( + PrivateDmEvent.prepareMessageToEncrypt(message, imetas), + toUser.pubKey, + ) - geohash?.let { geohash(it) } - zapRaiserAmount?.let { zapraiser(it) } - zapReceiver?.let { zapSplits(it) } - emojis?.let { emojis(it) } - contentWarningReason?.let { contentWarning(contentWarningReason) } - } + val template = + PrivateDmEvent.build(toUser, encryptedContent) { + replyingTo?.let { reply(it.toEId()) } - val destinationRelays = cache.getOrCreateUser(toUser.pubKey).dmInboxRelays() + geohash?.let { geohash(it) } + zapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + emojis?.let { emojis(it) } + contentWarningReason?.let { contentWarning(contentWarningReason) } + } - signAndSendNIP04Message(draftTag, template, outboxRelays.flow.value + destinationRelays) - } + val destinationRelays = cache.getOrCreateUser(toUser.pubKey).dmInboxRelays() + + signAndSendNIP04Message(draftTag, template, outboxRelays.flow.value + destinationRelays) } - fun sendNIP17EncryptedFile(template: EventTemplate) { + suspend fun sendNIP17EncryptedFile(template: EventTemplate) { if (!isWriteable()) return - NIP17Factory().createEncryptedFileNIP17(template, signer) { - broadcastPrivately(it) - } + val wraps = NIP17Factory().createEncryptedFileNIP17(template, signer) + broadcastPrivately(wraps) } - fun sendNIP17PrivateMessage( + suspend fun sendNIP17PrivateMessage( template: EventTemplate, draftTag: String? = null, ) { @@ -1642,16 +1522,14 @@ class Account( if (template.content.isEmpty()) { deleteDraft(draftTag) } else { - signer.assembleRumor(template) { - DraftEvent.create(draftTag, it, emptyList(), signer) { draftEvent -> - sendDraftEvent(draftEvent) - } - } + val rumor = RumorAssembler.assembleRumor(signer.pubKey, template) + val draftEvent = DraftEvent.create(draftTag, rumor, emptyList(), signer) + draftsDecryptionCache.preload(draftEvent, rumor) + sendDraftEvent(draftEvent) } } else { - NIP17Factory().createMessageNIP17(template, signer) { - broadcastPrivately(it) - } + val it = NIP17Factory().createMessageNIP17(template, signer) + broadcastPrivately(it) } } @@ -1659,7 +1537,9 @@ class Account( sendToPrivateOutboxAndLocal(draftEvent) } - fun sendToPrivateOutboxAndLocal(event: Event) { + fun sendToPrivateOutboxAndLocal(event: Event?) { + if (event == null) return + val relayList = privateStorageRelayList.flow.value + localRelayList.flow.value if (relayList.isNotEmpty()) { client.send(event, relayList.toSet()) @@ -1669,17 +1549,19 @@ class Account( cache.justConsumeMyOwnEvent(event) } - fun broadcastPrivately(signedEvents: NIP17Factory.Result) { + suspend fun broadcastPrivately(signedEvents: NIP17Factory.Result) { val mine = signedEvents.wraps.filter { (it.recipientPubKey() == signer.pubKey) } mine.forEach { giftWrap -> - giftWrap.unwrap(signer) { gift -> - if (gift is SealedRumorEvent) { - gift.unseal(signer) { rumor -> - cache.justConsumeMyOwnEvent(rumor) - } + val gift = giftWrap.unwrapOrNull(signer) + if (gift is SealedRumorEvent) { + val rumor = gift.unsealOrNull(signer) + if (rumor != null) { + cache.justConsumeMyOwnEvent(rumor) } + } + if (gift != null) { cache.justConsumeMyOwnEvent(gift) } @@ -1700,20 +1582,20 @@ class Account( } } - fun createStatus(newStatus: String) = UserStatusAction.create(newStatus, signer, ::sendMyPublicAndPrivateOutbox) + suspend fun createStatus(newStatus: String) = sendMyPublicAndPrivateOutbox(UserStatusAction.create(newStatus, signer)) - fun updateStatus( + suspend fun updateStatus( oldStatus: AddressableNote, newStatus: String, - ) = UserStatusAction.update(oldStatus, newStatus, signer, ::sendMyPublicAndPrivateOutbox) + ) = sendMyPublicAndPrivateOutbox(UserStatusAction.update(oldStatus, newStatus, signer)) - fun deleteStatus(oldStatus: AddressableNote) = UserStatusAction.delete(oldStatus, signer, ::sendMyPublicAndPrivateOutbox) + suspend fun deleteStatus(oldStatus: AddressableNote) = sendMyPublicAndPrivateOutbox(UserStatusAction.delete(oldStatus, signer)) - fun removeEmojiPack(emojiPack: Note) = emoji.removeEmojiPack(emojiPack, ::sendMyPublicAndPrivateOutbox) + suspend fun removeEmojiPack(emojiPack: Note) = sendMyPublicAndPrivateOutbox(emoji.removeEmojiPack(emojiPack)) - fun addEmojiPack(emojiPack: Note) = emoji.addEmojiPack(emojiPack, ::sendMyPublicAndPrivateOutbox) + suspend fun addEmojiPack(emojiPack: Note) = sendMyPublicAndPrivateOutbox(emoji.addEmojiPack(emojiPack)) - fun addToGallery( + suspend fun addToGallery( idHex: HexKey, url: String, relay: NormalizedRelayUrl?, @@ -1722,192 +1604,95 @@ class Account( hash: String?, mimeType: String?, ) { - if (!isWriteable()) return - - signer.sign( + val template = ProfileGalleryEntryEvent.build(url) { fromEvent(idHex, relay) hash?.let { hash(hash) } mimeType?.let { mimeType(it) } dim?.let { dimension(it) } blurhash?.let { blurhash(it) } - }, - ) { - client.send(it, outboxRelays.flow.value) - cache.justConsumeMyOwnEvent(it) - } + } + + val event = signer.sign(template) + sendMyPublicAndPrivateOutbox(event) } - fun removeFromGallery(note: Note) { + suspend fun removeFromGallery(note: Note) { delete(note) } - fun addBookmark( + fun isInPrivateBookmarks(note: Note) = bookmarkState.isInPrivateBookmarks(note) + + fun isInPublicBookmarks(note: Note) = bookmarkState.isInPublicBookmarks(note) + + suspend fun addBookmark( note: Note, isPrivate: Boolean, ) { if (!isWriteable()) return if (note.isDraft()) return - if (note is AddressableNote) { - BookmarkListEvent.addReplaceable( - userProfile().latestBookmarkList, - note.toATag(), - isPrivate, - signer, - ) { - client.send(it, outboxRelays.flow.value) - cache.justConsumeMyOwnEvent(it) - } - } else { - BookmarkListEvent.addEvent( - userProfile().latestBookmarkList, - note.idHex, - isPrivate, - signer, - ) { - client.send(it, outboxRelays.flow.value) - cache.justConsumeMyOwnEvent(it) - } - } + sendMyPublicAndPrivateOutbox(bookmarkState.addBookmark(note, isPrivate)) } - fun removeBookmark( + suspend fun removeBookmark( note: Note, isPrivate: Boolean, ) { if (!isWriteable()) return + if (note.isDraft()) return - val bookmarks = userProfile().latestBookmarkList ?: return - - if (note is AddressableNote) { - BookmarkListEvent.removeReplaceable( - bookmarks, - note.toATag(), - isPrivate, - signer, - ) { - client.send(it, outboxRelays.flow.value) - cache.justConsumeMyOwnEvent(it) - } - } else { - BookmarkListEvent.removeEvent( - bookmarks, - note.idHex, - isPrivate, - signer, - ) { - client.send(it, outboxRelays.flow.value) - cache.justConsumeMyOwnEvent(it) - } + val event = bookmarkState.removeBookmark(note, isPrivate) + if (event != null) { + sendMyPublicAndPrivateOutbox(event) } } - fun sendAuthEvent( + suspend fun sendAuthEvent( relay: IRelayClient, challenge: String, ) { - RelayAuthEvent.create(relay.url, challenge, signer) { - client.sendIfExists(it, relay.url) - } + val auth = RelayAuthEvent.create(relay.url, challenge, signer) + client.sendIfExists(auth, relay.url) } - fun isInPrivateBookmarks( - note: Note, - onReady: (Boolean) -> Unit, - ) { - if (!isWriteable()) { - onReady(false) - false - } - if (userProfile().latestBookmarkList == null) { - onReady(false) - false - } - - if (note is AddressableNote) { - userProfile().latestBookmarkList?.privateAddress(signer) { - onReady(it.contains(note.address)) - } - } else { - userProfile().latestBookmarkList?.privateTaggedEvents(signer) { - onReady(it.contains(note.idHex)) - } - } + suspend fun hideWord(word: String) { + sendMyPublicAndPrivateOutbox(muteList.hideWord(word)) } - fun isInPublicBookmarks(note: Note): Boolean { - if (!isWriteable()) return false - - return if (note is AddressableNote) { - userProfile().latestBookmarkList?.isTaggedAddressableNote(note.idHex) == true - } else { - userProfile().latestBookmarkList?.isTaggedEvent(note.idHex) == true - } + suspend fun showWord(word: String) { + sendMyPublicAndPrivateOutbox(blockPeopleList.showWord(word)) + sendMyPublicAndPrivateOutbox(muteList.showWord(word)) } - fun hideWord(word: String) { - if (!isWriteable()) return - - muteList.hideWord(word, ::sendMyPublicAndPrivateOutbox) + suspend fun hideUser(pubkeyHex: HexKey) { + sendMyPublicAndPrivateOutbox(muteList.hideUser(pubkeyHex)) } - fun showWord(word: String) { - if (!isWriteable()) return - - blockPeopleList.showWord(word, ::sendMyPublicAndPrivateOutbox) - muteList.showWord(word, ::sendMyPublicAndPrivateOutbox) - } - - fun hideUser(pubkeyHex: HexKey) { - if (!isWriteable()) return - - muteList.hideUser(pubkeyHex, ::sendMyPublicAndPrivateOutbox) - } - - fun showUser(pubkeyHex: HexKey) { - if (!isWriteable()) return - - blockPeopleList.showUser(pubkeyHex, ::sendMyPublicAndPrivateOutbox) - muteList.showUser(pubkeyHex, ::sendMyPublicAndPrivateOutbox) + suspend fun showUser(pubkeyHex: HexKey) { + sendMyPublicAndPrivateOutbox(blockPeopleList.showUser(pubkeyHex)) + sendMyPublicAndPrivateOutbox(muteList.showUser(pubkeyHex)) hiddenUsers.showUser(pubkeyHex) } - fun requestDVMContentDiscovery( + suspend fun requestDVMContentDiscovery( dvmPublicKey: User, onReady: (event: NIP90ContentDiscoveryRequestEvent) -> Unit, ) { val relays = nip65RelayList.inboxFlow.value.toSet() - NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey.pubkeyHex, signer.pubKey, relays, signer) { - val relayList = - dvmPublicKey - .inboxRelays() - .ifEmpty { - LocalCache.relayHints.hintsForKey(dvmPublicKey.pubkeyHex) + dvmPublicKey.relaysBeingUsed.keys - }.toSet() + val request = NIP90ContentDiscoveryRequestEvent.create(dvmPublicKey.pubkeyHex, signer.pubKey, relays, signer) - cache.justConsumeMyOwnEvent(it) - onReady(it) - client.send(it, relayList) - } - } + val relayList = + dvmPublicKey + .inboxRelays() + .ifEmpty { + cache.relayHints.hintsForKey(dvmPublicKey.pubkeyHex) + dvmPublicKey.relaysBeingUsed.keys + }.toSet() - fun unwrap( - event: GiftWrapEvent, - onReady: (Event) -> Unit, - ) { - if (!isWriteable()) return - - return event.unwrap(signer, onReady) - } - - fun unseal( - event: SealedRumorEvent, - onReady: (Event) -> Unit, - ) { - if (!isWriteable()) return - - return event.unseal(signer, onReady) + cache.justConsumeMyOwnEvent(request) + onReady(request) + delay(100) + client.send(request, relayList) } fun cachedDecryptContent(note: Note): String? = cachedDecryptContent(note.event) @@ -1917,11 +1702,11 @@ class Account( return if (isWriteable()) { if (event is PrivateDmEvent) { - event.cachedContentFor(signer) + privateDMDecryptionCache.cachedDM(event) } else if (event is LnZapRequestEvent && event.isPrivateZap()) { - event.cachedPrivateZap()?.content + privateZapsDecryptionCache.cachedPrivateZap(event)?.content } else if (event is DraftEvent) { - event.preCachedDraft(signer)?.content + draftsDecryptionCache.preCachedDraft(event)?.content } else { event.content } @@ -1930,48 +1715,31 @@ class Account( } } - fun decryptContent( - note: Note, - onReady: (String) -> Unit, - ) { + suspend fun decryptContent(note: Note): String? { val event = note.event - if (event is PrivateDmEvent && isWriteable()) { - event.plainContent(signer, onReady) - } else if (event is LnZapRequestEvent) { - decryptZapContentAuthor(note) { onReady(it.content) } - } else if (event is DraftEvent) { - event.cachedDraft(signer) { - onReady(it.content) - } - } else { - event?.content?.let { onReady(it) } - } - } - - fun decryptZapContentAuthor( - note: Note, - onReady: (Event) -> Unit, - ) { - val event = note.event - if (event is LnZapRequestEvent) { + return if (event is PrivateDmEvent && isWriteable()) { + privateDMDecryptionCache.decryptDM(event) + } else if (event is LnZapRequestEvent && isWriteable()) { if (event.isPrivateZap()) { if (isWriteable()) { - event.decryptPrivateZap(signer) { onReady(it) } + privateZapsDecryptionCache.decryptPrivateZap(event)?.content + } else { + null } } else { - onReady(event) + event.content } + } else if (event is DraftEvent && isWriteable()) { + draftsDecryptionCache.cachedDraft(event)?.content + } else { + event?.content } } suspend fun decryptZapOrNull(event: LnZapRequestEvent): LnZapPrivateEvent? = if (event.isPrivateZap()) { if (isWriteable()) { - tryAndWait { continuation -> - event.decryptPrivateZap(signer) { - continuation.resume(it) - } - } + privateZapsDecryptionCache.decryptPrivateZap(event) } else { null } @@ -2049,21 +1817,21 @@ class Account( ).toSet() } - fun saveDMRelayList(dmRelays: List) = dmRelayList.saveRelayList(dmRelays, ::sendMyPublicAndPrivateOutbox) + suspend fun saveDMRelayList(dmRelays: List) = sendEverywhere(dmRelayList.saveRelayList(dmRelays)) - fun savePrivateOutboxRelayList(relays: List) = privateStorageRelayList.saveRelayList(relays, ::sendMyPublicAndPrivateOutbox) + suspend fun savePrivateOutboxRelayList(relays: List) = sendMyPublicAndPrivateOutbox(privateStorageRelayList.saveRelayList(relays)) - fun saveSearchRelayList(searchRelays: List) = searchRelayList.saveRelayList(searchRelays, ::sendMyPublicAndPrivateOutbox) + suspend fun saveSearchRelayList(searchRelays: List) = sendMyPublicAndPrivateOutbox(searchRelayList.saveRelayList(searchRelays)) - fun saveTrustedRelayList(trustedRelays: List) = trustedRelayList.saveRelayList(trustedRelays, ::sendMyPublicAndPrivateOutbox) + suspend fun saveTrustedRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(trustedRelayList.saveRelayList(trustedRelays)) - fun saveBlockedRelayList(blockedRelays: List) = blockedRelayList.saveRelayList(blockedRelays, ::sendMyPublicAndPrivateOutbox) + suspend fun saveBlockedRelayList(blockedRelays: List) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays)) - fun sendNip65RelayList(relays: List) = nip65RelayList.saveRelayList(relays, ::sendMyPublicAndPrivateOutbox) + suspend fun sendNip65RelayList(relays: List) = sendEverywhere(nip65RelayList.saveRelayList(relays)) - fun sendFileServersList(servers: List) = fileStorageServers.saveFileServersList(servers, ::sendMyPublicAndPrivateOutbox) + suspend fun sendFileServersList(servers: List) = sendMyPublicAndPrivateOutbox(fileStorageServers.saveFileServersList(servers)) - fun sendBlossomServersList(servers: List) = blossomServers.saveBlossomServersList(servers, ::sendMyPublicAndPrivateOutbox) + suspend fun sendBlossomServersList(servers: List) = sendMyPublicAndPrivateOutbox(blossomServers.saveBlossomServersList(servers)) fun getAllPeopleLists(): List = getAllPeopleLists(signer.pubKey) @@ -2073,7 +1841,7 @@ class Account( val noteEvent = addressableNote.event if (noteEvent is PeopleListEvent) { - noteEvent.pubKey == pubkey && (noteEvent.hasAnyTaggedUser() || noteEvent.cachedPrivateTags()?.isNotEmpty() == true) + noteEvent.pubKey == pubkey && (noteEvent.hasAnyTaggedUser() || peopleListDecryptionCache.cachedUserIdSet(noteEvent).isNotEmpty()) } else if (noteEvent is FollowListEvent) { noteEvent.pubKey == pubkey && noteEvent.hasAnyTaggedUser() } else { @@ -2221,10 +1989,11 @@ class Account( } scope.launch(Dispatchers.Default) { + Log.d("DB UPGRADE", "Migrating List") delay(1000 * 60 * 1) // waits 5 minutes before migrating the list. val contactList = userProfile().latestContactList - val oldChannels = contactList?.taggedEventIds()?.toSet()?.mapNotNull { cache.getPublicChatChannelIfExists(it) as? PublicChatChannel } + val oldChannels = contactList?.taggedEventIds()?.toSet()?.mapNotNull { cache.getPublicChatChannelIfExists(it) } if (oldChannels != null && oldChannels.isNotEmpty()) { Log.d("DB UPGRADE", "Migrating List with ${oldChannels.size} old channels ") @@ -2236,7 +2005,9 @@ class Account( if (needsToUpgrade.isNotEmpty()) { Log.d("DB UPGRADE", "Migrating List") - publicChatList.follow(oldChannels, ::sendMyPublicAndPrivateOutbox) + runCatching { + sendMyPublicAndPrivateOutbox(publicChatList.follow(oldChannels)) + } } } @@ -2252,7 +2023,9 @@ class Account( if (needsToUpgrade.isNotEmpty()) { Log.d("DB UPGRADE", "Migrating List") - communityList.follow(oldCommunities, ::sendMyPublicAndPrivateOutbox) + runCatching { + sendMyPublicAndPrivateOutbox(communityList.follow(oldCommunities)) + } } } @@ -2268,14 +2041,15 @@ class Account( if (needsToUpgrade.isNotEmpty()) { Log.d("DB UPGRADE", "Migrating List") - hashtagList.follow(oldHashtags.toList(), ::sendMyPublicAndPrivateOutbox) + runCatching { + sendMyPublicAndPrivateOutbox(hashtagList.follow(oldHashtags.toList())) + } } } val oldGeohashes = contactList?.geohashes()?.toSet() - if (oldGeohashes != null && oldGeohashes.isNotEmpty()) { - Log.d("DB UPGRADE", "Migrating List with ${oldGeohashes.size} old communities ") + Log.d("DB UPGRADE", "Migrating List with ${oldGeohashes.size} old geohashes ") val existingGeohashes = geohashList.flow.value val needsToUpgrade = oldGeohashes.filter { it !in existingGeohashes } @@ -2284,7 +2058,9 @@ class Account( if (needsToUpgrade.isNotEmpty()) { Log.d("DB UPGRADE", "Migrating List") - geohashList.follow(oldGeohashes.toList(), ::sendMyPublicAndPrivateOutbox) + runCatching { + sendMyPublicAndPrivateOutbox(geohashList.follow(oldGeohashes.toList())) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 58dc27e8b8..0c0b467fab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -27,26 +27,26 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.screen.FeedDefinition import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow -import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent -import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal @@ -66,9 +66,9 @@ import java.util.Locale val DefaultChannels = listOf( // Anigma's Nostr - EventIdHint("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", Constants.nos), + ChannelTag("25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", Constants.nos), // Amethyst's Group - EventIdHint("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", Constants.nos), + ChannelTag("42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5", Constants.nos), ) val DefaultNIP65RelaySet = setOf(Constants.mom, Constants.nos, Constants.bitcoiner) @@ -115,7 +115,7 @@ class AccountSettings( val defaultStoriesFollowList: MutableStateFlow = MutableStateFlow(GLOBAL_FOLLOWS), val defaultNotificationFollowList: MutableStateFlow = MutableStateFlow(GLOBAL_FOLLOWS), val defaultDiscoveryFollowList: MutableStateFlow = MutableStateFlow(GLOBAL_FOLLOWS), - var zapPaymentRequest: Nip47WalletConnect.Nip47URINorm? = null, + var zapPaymentRequest: MutableStateFlow = MutableStateFlow(null), var hideDeleteRequestDialog: Boolean = false, var hideBlockAlertDialog: Boolean = false, var hideNIP17WarningDialog: Boolean = false, @@ -199,8 +199,8 @@ class AccountSettings( } fun changeZapPaymentRequest(newServer: Nip47WalletConnect.Nip47URINorm?): Boolean { - if (zapPaymentRequest != newServer) { - zapPaymentRequest = newServer + if (zapPaymentRequest.value != newServer) { + zapPaymentRequest.tryEmit(newServer) saveAccountSettings() return true } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index 5029f6044f..16d1a76260 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -138,11 +138,12 @@ class AccountLanguagePreferences( // language services // --- fun toggleDontTranslateFrom(languageCode: String) { - if (!dontTranslateFrom.contains(languageCode)) { - dontTranslateFrom = dontTranslateFrom.plus(languageCode) - } else { - dontTranslateFrom = dontTranslateFrom.minus(languageCode) - } + dontTranslateFrom = + if (!dontTranslateFrom.contains(languageCode)) { + dontTranslateFrom.plus(languageCode) + } else { + dontTranslateFrom.minus(languageCode) + } } fun translateToContains(languageCode: Locale) = translateTo.contains(languageCode.language) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 7493edddd1..7e6936fdc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -168,7 +168,7 @@ abstract class Channel { var creator: User? = null var updatedMetadataAt: Long = 0 val notes = LargeCache() - var lastNoteCreatedAt: Long = 0 + var lastNote: Note? = null private var relays = mapOf() @@ -220,8 +220,8 @@ abstract class Channel { ) { notes.put(note.idHex, note) - if ((note.createdAt() ?: 0) > lastNoteCreatedAt) { - lastNoteCreatedAt = note.createdAt() ?: 0 + if ((note.createdAt() ?: 0) > (lastNote?.createdAt() ?: 0)) { + lastNote = note } if (relay != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index a50c55a562..2bf690c518 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -28,12 +28,12 @@ import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag +import com.vitorpamplona.amethyst.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.dateFormatter import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId @@ -83,7 +83,6 @@ import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionIndex import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent @@ -120,6 +119,7 @@ import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip40Expiration.isExpirationBefore @@ -127,16 +127,16 @@ import com.vitorpamplona.quartz.nip40Expiration.isExpired import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent -import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent -import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent @@ -178,12 +178,11 @@ import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.LargeCache import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -210,11 +209,12 @@ object LocalCache : ILocalCache { val notes = LargeCache() val addressables = LargeCache() + val chatroomList = LargeCache() val publicChatChannels = LargeCache() val liveChatChannels = LargeCache() val ephemeralChannels = LargeCache() - val awaitingPaymentRequests = ConcurrentHashMap Unit>>(10) + val awaitingPaymentRequests = ConcurrentHashMap Unit>>(10) val relayHints = HintIndexer() @@ -223,8 +223,6 @@ object LocalCache : ILocalCache { val observablesByKindAndETag = ConcurrentHashMap>>(10) val observablesByKindAndAuthor = ConcurrentHashMap>>(10) - val onNewEvents = mutableListOf<(Note) -> Unit>() - fun observeETag( kind: Int, eventId: HexKey, @@ -321,6 +319,8 @@ object LocalCache : ILocalCache { fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId) + fun getChatroomListIfExists(key: String): ChatroomList? = chatroomList.get(key) + fun getPublicChatChannelIfExists(key: String): PublicChatChannel? = publicChatChannels.get(key) fun getEphemeralChatChannelIfExists(key: RoomId): EphemeralChatChannel? = ephemeralChannels.get(key) @@ -400,20 +400,13 @@ object LocalCache : ILocalCache { } } - fun getOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = - publicChatChannels.getOrCreate(key) { - PublicChatChannel(key) - } + fun getOrCreateChatroomList(key: HexKey): ChatroomList = chatroomList.getOrCreate(key) { ChatroomList(key) } - fun getOrCreateLiveChannel(key: Address): LiveActivitiesChannel = - liveChatChannels.getOrCreate(key) { - LiveActivitiesChannel(key) - } + fun getOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = publicChatChannels.getOrCreate(key) { PublicChatChannel(key) } - fun getOrCreateEphemeralChannel(key: RoomId): EphemeralChatChannel = - ephemeralChannels.getOrCreate(key) { - EphemeralChatChannel(key) - } + fun getOrCreateLiveChannel(key: Address): LiveActivitiesChannel = liveChatChannels.getOrCreate(key) { LiveActivitiesChannel(key) } + + fun getOrCreateEphemeralChannel(key: RoomId): EphemeralChatChannel = ephemeralChannels.getOrCreate(key) { EphemeralChatChannel(key) } fun checkGetOrCreatePublicChatChannel(key: String): PublicChatChannel? { if (isValidHex(key)) { @@ -521,19 +514,7 @@ object LocalCache : ILocalCache { event: BookmarkListEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, - ): Boolean { - val user = getOrCreateUser(event.pubKey) - if (user.latestBookmarkList == null || event.createdAt > user.latestBookmarkList!!.createdAt) { - if (event.dTag() == "bookmark") { - if (wasVerified || justVerify(event)) { - user.updateBookmark(event) - return true - } - } - } - - return false - } + ) = consumeBaseReplaceable(event, relay, wasVerified) fun formattedDateTime(timestamp: Long): String = Instant @@ -599,7 +580,7 @@ object LocalCache : ILocalCache { // Counts the replies replyTo.forEach { it.addReply(note) } - refreshObservers(note) + refreshNewNoteObservers(note) return true } else { @@ -711,7 +692,7 @@ object LocalCache : ILocalCache { if (event.createdAt > (note.createdAt() ?: 0)) { note.loadEvent(event, author, replyTo) - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -756,7 +737,7 @@ object LocalCache : ILocalCache { note.loadEvent(event, author, replyTo) - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -864,7 +845,7 @@ object LocalCache : ILocalCache { channel.updateChannelInfo(creator, event, event.createdAt) - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1054,7 +1035,7 @@ object LocalCache : ILocalCache { author.flowSet?.statuses?.invalidateData() - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1085,7 +1066,7 @@ object LocalCache : ILocalCache { version.flowSet?.ots?.invalidateData() } - refreshObservers(version) + refreshNewNoteObservers(version) return true } @@ -1175,7 +1156,7 @@ object LocalCache : ILocalCache { if (event.createdAt > (replaceableNote.createdAt() ?: 0) && (isVerified || justVerify(event))) { replaceableNote.loadEvent(event, author, computeReplyTo(event)) - refreshObservers(replaceableNote) + refreshNewNoteObservers(replaceableNote) return true } else { @@ -1199,39 +1180,7 @@ object LocalCache : ILocalCache { event: PrivateDmEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, - ): Boolean { - 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 false - - if (wasVerified || justVerify(event)) { - val recipient = event.verifiedRecipientPubKey()?.let { getOrCreateUser(it) } - - // Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") - - val repliesTo = computeReplyTo(event) - - note.loadEvent(event, author, repliesTo) - - if (recipient != null) { - author.addMessage(recipient, note) - recipient.addMessage(author, note) - } - - refreshObservers(note) - - return true - } - - return false - } + ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: DeletionEvent, @@ -1300,7 +1249,7 @@ object LocalCache : ILocalCache { } } - refreshObservers(note) + refreshNewNoteObservers(note) return true } else { @@ -1350,26 +1299,7 @@ object LocalCache : ILocalCache { getNoteIfExists(it)?.removeReply(deleteNote) } - if (deletedEvent is PrivateDmEvent) { - val author = deleteNote.author - val recipient = - deletedEvent.verifiedRecipientPubKey()?.let { - checkGetOrCreateUser(it) - } - - if (recipient != null && author != null) { - author.removeMessage(recipient, deleteNote) - recipient.removeMessage(author, deleteNote) - } - } - - if (deletedEvent is DraftEvent) { - deletedEvent.allCache().forEach { - it?.let { - deindexDraftAsRealEvent(deleteNote, it) - } - } - } + notes.remove(deleteNote.idHex) if (deletedEvent is WrappedEvent) { deleteWraps(deletedEvent) @@ -1377,7 +1307,7 @@ object LocalCache : ILocalCache { deleteNote.clearFlow() - notes.remove(deleteNote.idHex) + refreshDeletedNoteObservers(deleteNote) } fun deleteWraps(event: WrappedEvent) { @@ -1389,6 +1319,7 @@ object LocalCache : ILocalCache { deleteWraps(noteEvent) } it.clearFlow() + refreshDeletedNoteObservers(it) } notes.remove(it.id) @@ -1418,7 +1349,7 @@ object LocalCache : ILocalCache { justConsumeAndUpdateIndexes(it, relay, false) } - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1448,7 +1379,7 @@ object LocalCache : ILocalCache { justConsumeAndUpdateIndexes(it, relay, false) } - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1483,7 +1414,7 @@ object LocalCache : ILocalCache { justConsumeAndUpdateIndexes(it, relay, false) } - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1509,7 +1440,7 @@ object LocalCache : ILocalCache { repliesTo.forEach { it.addReaction(note) } - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1553,7 +1484,7 @@ object LocalCache : ILocalCache { } } - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1576,7 +1507,7 @@ object LocalCache : ILocalCache { oldChannel.addNote(note, relay) note.loadEvent(event, author, emptyList()) - refreshObservers(note) + refreshNewNoteObservers(note) true } else { wasVerified @@ -1624,7 +1555,7 @@ object LocalCache : ILocalCache { oldChannel.addNote(note, relay) note.loadEvent(event, author, emptyList()) - refreshObservers(note) + refreshNewNoteObservers(note) } return isVerified @@ -1709,7 +1640,6 @@ object LocalCache : ILocalCache { if (wasVerified || justVerify(event)) { val existingZapRequest = event.zapRequest?.id?.let { getNoteIfExists(it) } - if (existingZapRequest == null || existingZapRequest.event == null) { // tries to add it event.zapRequest?.let { @@ -1733,7 +1663,7 @@ object LocalCache : ILocalCache { repliesTo.forEach { it.addZap(zapRequest, note) } mentions.forEach { it.addZap(zapRequest, note) } - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1761,7 +1691,7 @@ object LocalCache : ILocalCache { repliesTo.forEach { it.addZap(note, null) } mentions.forEach { it.addZap(note, null) } - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1826,7 +1756,7 @@ object LocalCache : ILocalCache { } } - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1885,7 +1815,7 @@ object LocalCache : ILocalCache { note.loadEvent(eventNoData, author, emptyList()) - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -1897,150 +1827,25 @@ object LocalCache : ILocalCache { event: ChatMessageEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, - ): Boolean { - 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 false - - if (wasVerified || justVerify(event)) { - val recipientsHex = event.groupMembers() - val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet() - - // Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") - - val repliesTo = computeReplyTo(event) - - note.loadEvent(event, author, repliesTo) - - if (recipients.isNotEmpty()) { - recipients.forEach { - val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex) - - val authorGroup = - if (groupMinusRecipient.isEmpty()) { - // note to self - ChatroomKey(persistentSetOf(it.pubkeyHex)) - } else { - ChatroomKey(groupMinusRecipient.toImmutableSet()) - } - - it.addMessage(authorGroup, note) - } - } - - refreshObservers(note) - - return true - } - - return false - } + ) = consumeRegularEvent(event, relay, wasVerified) private fun consume( event: ChatMessageEncryptedFileHeaderEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, - ): Boolean { - 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 false - - if (wasVerified || justVerify(event)) { - val recipientsHex = event.groupMembers() - val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet() - - // Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") - - val repliesTo = computeReplyTo(event) - - note.loadEvent(event, author, repliesTo) - - if (recipients.isNotEmpty()) { - recipients.forEach { - val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex) - - val authorGroup = - if (groupMinusRecipient.isEmpty()) { - // note to self - ChatroomKey(persistentSetOf(it.pubkeyHex)) - } else { - ChatroomKey(groupMinusRecipient.toImmutableSet()) - } - - it.addMessage(authorGroup, note) - } - } - - refreshObservers(note) - - return true - } - - return false - } + ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: SealedRumorEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, - ): Boolean { - 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 false - - if (wasVerified || justVerify(event)) { - note.loadEvent(event, author, emptyList()) - refreshObservers(note) - return true - } - - return false - } + ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: GiftWrapEvent, relay: NormalizedRelayUrl?, wasVerified: Boolean, - ): Boolean { - val note = getOrCreateNote(event.id) - val author = getOrCreateUser(event.pubKey) - - if (relay != null) { - note.addRelay(relay) - } - - // Already processed this event. - if (note.event != null) return false - - if (wasVerified || justVerify(event)) { - note.loadEvent(event, author, emptyList()) - refreshObservers(note) - return true - } - - return false - } + ) = consumeRegularEvent(event, relay, wasVerified) fun consume( event: LnZapPaymentRequestEvent, @@ -2056,7 +1861,7 @@ object LocalCache : ILocalCache { zappedNote: Note?, wasVerified: Boolean, relay: NormalizedRelayUrl?, - onResponse: (LnZapPaymentResponseEvent) -> Unit, + onResponse: suspend (LnZapPaymentResponseEvent) -> Unit, ): Boolean { val note = getOrCreateNote(event.id) val author = getOrCreateUser(event.pubKey) @@ -2075,7 +1880,7 @@ object LocalCache : ILocalCache { awaitingPaymentRequests.put(event.id, Pair(zappedNote, onResponse)) - refreshObservers(note) + refreshNewNoteObservers(note) return true } @@ -2106,7 +1911,9 @@ object LocalCache : ILocalCache { requestNote?.let { request -> zappedNote?.addZapPayment(request, note) } - responseCallback(event) + GlobalScope.launch(Dispatchers.Default) { + responseCallback(event) + } return true } @@ -2438,9 +2245,9 @@ object LocalCache : ILocalCache { pruneOldMessagesChannel(channel) } - users.forEach { _, user -> - user.privateChatrooms.values.map { - val toBeRemoved = it.pruneMessagesToTheLatestOnly() + chatroomList.forEach { userHex, room -> + room.chatrooms.map { key, chatroom -> + val toBeRemoved = chatroom.pruneMessagesToTheLatestOnly() val childrenToBeRemoved = mutableListOf() @@ -2456,7 +2263,7 @@ object LocalCache : ILocalCache { if (toBeRemoved.size > 1) { println( - "PRUNE: ${toBeRemoved.size} private messages from ${user.toBestDisplayName()} to ${it.authors.joinToString(", ") { it.toBestDisplayName() }} removed. ${it.roomMessages.size} kept", + "PRUNE: ${toBeRemoved.size} private messages from $userHex to ${key.users.joinToString()} removed. ${chatroom.roomMessages.size} kept", ) } } @@ -2582,6 +2389,8 @@ object LocalCache : ILocalCache { note.clearFlow() notes.remove(note.idHex) + + refreshDeletedNoteObservers(note) } fun removeFromCache(nextToBeRemoved: List) { @@ -2665,11 +2474,14 @@ object LocalCache : ILocalCache { // Observers line up here. val live: LocalCacheFlow = LocalCacheFlow() - private fun refreshObservers(newNote: Note) { + private fun refreshNewNoteObservers(newNote: Note) { val event = newNote.event as Event updateObservables(event) - onNewEvents.forEach { it(newNote) } - live.invalidateData(newNote) + live.newNote(newNote) + } + + private fun refreshDeletedNoteObservers(newNote: Note) { + live.removedNote(newNote) } fun justVerify(event: Event): Boolean { @@ -2680,7 +2492,7 @@ object LocalCache : ILocalCache { event.checkSignature() } catch (e: Exception) { if (e is CancellationException) throw e - Log.w("Event Verification Failed", "Kind: ${event.kind} from ${dateFormatter(event.createdAt, "", "")} with message ${e.message}: ${event.toJson()}") + Log.w("Event Verification Failed", "Kind: ${event.kind} from ${dateFormatter(event.createdAt, "", "")} with message ${e.message}") } false } else { @@ -2702,165 +2514,12 @@ object LocalCache : ILocalCache { val note = Note(event.id) note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList()) relay?.let { note.addRelay(it) } - refreshObservers(note) + refreshNewNoteObservers(note) } return false } - fun indexDraftAsRealEvent( - draftWrap: DraftEvent, - draft: Event, - ) { - val note = getOrCreateAddressableNote(draftWrap.address()) - val author = getOrCreateUser(draftWrap.pubKey) - - when (draft) { - is PrivateDmEvent -> { - draft.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }?.let { recipient -> - author.addMessage(recipient, note) - recipient.addMessage(author, note) - } - } - is ChatMessageEvent -> { - val recipientsHex = draft.groupMembers() - val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet() - - if (recipients.isNotEmpty()) { - recipients.forEach { - val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex) - - val authorGroup = - if (groupMinusRecipient.isEmpty()) { - // note to self - ChatroomKey(persistentSetOf(it.pubkeyHex)) - } else { - ChatroomKey(groupMinusRecipient.toImmutableSet()) - } - - it.addMessage(authorGroup, note) - } - } - } - is ChatMessageEncryptedFileHeaderEvent -> { - val recipientsHex = draft.groupMembers() - val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet() - - if (recipients.isNotEmpty()) { - recipients.forEach { - val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex) - - val authorGroup = - if (groupMinusRecipient.isEmpty()) { - // note to self - ChatroomKey(persistentSetOf(it.pubkeyHex)) - } else { - ChatroomKey(groupMinusRecipient.toImmutableSet()) - } - - it.addMessage(authorGroup, note) - } - } - } - is EphemeralChatEvent -> { - draft.roomId()?.let { - getOrCreateEphemeralChannel(it).addNote(note, null) - } - } - is ChannelMessageEvent -> { - draft.channelId()?.let { channelId -> - checkGetOrCreatePublicChatChannel(channelId)?.addNote(note, null) - } - } - is LiveActivitiesChatMessageEvent -> { - draft.activityAddress()?.let { channelId -> - getOrCreateLiveChannel(channelId).addNote(note, null) - } - } - is TextNoteEvent -> { - val replyTo = computeReplyTo(draft) - val author = getOrCreateUser(draftWrap.pubKey) - note.loadEvent(draftWrap, author, replyTo) - replyTo.forEach { it.addReply(note) } - } - } - } - - fun deindexDraftAsRealEvent( - draftWrap: Note, - draft: Event, - ) { - val author = draftWrap.author ?: return - - when (draft) { - is PrivateDmEvent -> { - draft.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }?.let { recipient -> - author.removeMessage(recipient, draftWrap) - recipient.removeMessage(author, draftWrap) - } - } - is ChatMessageEvent -> { - val recipientsHex = draft.recipientsPubKey().plus(author.pubkeyHex).toSet() - val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet() - - if (recipients.isNotEmpty()) { - recipients.forEach { - val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex) - - val authorGroup = - if (groupMinusRecipient.isEmpty()) { - // note to self - ChatroomKey(persistentSetOf(it.pubkeyHex)) - } else { - ChatroomKey(groupMinusRecipient.toImmutableSet()) - } - - it.removeMessage(authorGroup, draftWrap) - } - } - } - is ChatMessageEncryptedFileHeaderEvent -> { - val recipientsHex = draft.groupMembers() - val recipients = recipientsHex.mapNotNull { checkGetOrCreateUser(it) }.toSet() - - if (recipients.isNotEmpty()) { - recipients.forEach { - val groupMinusRecipient = recipientsHex.minus(it.pubkeyHex) - - val authorGroup = - if (groupMinusRecipient.isEmpty()) { - // note to self - ChatroomKey(persistentSetOf(it.pubkeyHex)) - } else { - ChatroomKey(groupMinusRecipient.toImmutableSet()) - } - - it.removeMessage(authorGroup, draftWrap) - } - } - } - is ChannelMessageEvent -> { - draft.channelId()?.let { channelId -> - getPublicChatChannelIfExists(channelId)?.removeNote(draftWrap) - } - } - is EphemeralChatEvent -> { - draft.roomId()?.let { - getEphemeralChatChannelIfExists(it)?.removeNote(draftWrap) - } - } - is LiveActivitiesChatMessageEvent -> { - draft.activityAddress()?.let { channelId -> - getLiveActivityChannelIfExists(channelId)?.removeNote(draftWrap) - } - } - is TextNoteEvent -> { - val replyTo = computeReplyTo(draft) - replyTo.forEach { it.removeReply(draftWrap) } - } - } - } - fun consume(nip19: Entity) { when (nip19) { is NSec -> getOrCreateUser(nip19.toPubKeyHex()) @@ -3122,7 +2781,7 @@ object LocalCache : ILocalCache { } } catch (e: Exception) { if (e is CancellationException) throw e - e.printStackTrace() + Log.w("LocalCache", "Cannot consume ${event.kind}", e) false } @@ -3159,15 +2818,24 @@ object LocalCache : ILocalCache { @Stable class LocalCacheFlow { - private val _newEventBundles = MutableSharedFlow>(0, 10, BufferOverflow.DROP_OLDEST) + private val _newEventBundles = MutableSharedFlow>(0, 100, BufferOverflow.DROP_OLDEST) val newEventBundles = _newEventBundles.asSharedFlow() // read-only public view + private val _deletedEventBundles = MutableSharedFlow>(0, 100, BufferOverflow.DROP_OLDEST) + val deletedEventBundles = _deletedEventBundles.asSharedFlow() // read-only public view + // Refreshes observers in batches. private val bundler = BundledInsert(1000, Dispatchers.Default) - fun invalidateData(newNote: Note) { + fun newNote(newNote: Note) { bundler.invalidateList(newNote) { bundledNewNotes -> _newEventBundles.emit(bundledNewNotes) } } + + fun removedNote(newNote: Note) { + bundler.invalidateList(newNote) { bundledNewNotes -> + _deletedEventBundles.emit(bundledNewNotes) + } + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 4594fa3402..5f5d3fbd94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji @@ -35,7 +36,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.events.ETag @@ -55,6 +55,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent @@ -71,14 +72,11 @@ import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.anyAsync import com.vitorpamplona.quartz.utils.containsAny import com.vitorpamplona.quartz.utils.launchAndWaitAll -import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.CancellationException import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flatMapLatest import java.math.BigDecimal -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume @Stable class AddressableNote( @@ -466,21 +464,13 @@ open class Note( val zapResponseEvent = next.second?.event as? LnZapPaymentResponseEvent if (zapResponseEvent != null) { - val result = - tryAndWait { continuation -> - account.decryptZapPaymentResponseEvent(zapResponseEvent) { response -> - if ( - response is PayInvoiceSuccessResponse && - account.isNIP47Author(zapResponseEvent.requestAuthor()) - ) { - continuation.resume(true) - } - } - } + account.nip47SignerState.decryptResponse(zapResponseEvent)?.let { response -> + val result = response is PayInvoiceSuccessResponse && account.nip47SignerState.isNIP47Author(zapResponseEvent.requestAuthor()) - if (!hasSentOne && result == true) { - hasSentOne = true - onWasZappedByAuthor() + if (!hasSentOne && result == true) { + hasSentOne = true + onWasZappedByAuthor() + } } } } @@ -513,7 +503,7 @@ open class Note( // private events // if has already decrypted - val privateZap = zapRequest.cachedPrivateZap() + val privateZap = account.privateZapsDecryptionCache.cachedPrivateZap(zapRequest) if (privateZap != null) { if (privateZap.pubKey == user.pubkeyHex && (option == null || option == zapEvent?.zappedPollOption())) { onWasZappedByAuthor() @@ -529,12 +519,7 @@ open class Note( val result = anyAsync(parallelDecrypt) { pair -> - val result = - tryAndWait { continuation -> - pair.first.decryptPrivateZap(account.signer) { - continuation.resume(it) - } - } + val result = account.privateZapsDecryptionCache.decryptPrivateZap(pair.first) result?.pubKey == user.pubkeyHex && (option == null || option == pair.second?.zappedPollOption()) } @@ -607,26 +592,21 @@ open class Note( startAmount: BigDecimal, paidInvoiceSet: LinkedHashSet, zapPayments: List>, - signer: NostrSigner, - onReady: (BigDecimal) -> Unit, - ) { + signerState: NwcSignerState, + ): BigDecimal { if (zapPayments.isEmpty()) { - onReady(startAmount) - return + return startAmount } var output: BigDecimal = startAmount launchAndWaitAll(zapPayments) { next -> val result = - tryAndWait { continuation -> - processZapAmountFromResponse( - next.first, - next.second, - continuation, - signer, - ) - } + processZapAmountFromResponse( + next.first, + next.second, + signerState, + ) if (result != null && !paidInvoiceSet.contains(result.invoice)) { paidInvoiceSet.add(result.invoice) @@ -634,27 +614,25 @@ open class Note( } } - onReady(output) + return output } - private fun processZapAmountFromResponse( + private suspend fun processZapAmountFromResponse( paymentRequest: Note, paymentResponse: Note?, - continuation: Continuation, - signer: NostrSigner, - ) { + signerState: NwcSignerState, + ): InvoiceAmount? { val nwcRequest = paymentRequest.event as? LnZapPaymentRequestEvent val nwcResponse = paymentResponse?.event as? LnZapPaymentResponseEvent - if (nwcRequest != null && nwcResponse != null) { + return if (nwcRequest != null && nwcResponse != null) { processZapAmountFromResponse( nwcRequest, nwcResponse, - continuation, - signer, + signerState, ) } else { - continuation.resume(null) + null } } @@ -663,18 +641,19 @@ open class Note( val amount: BigDecimal, ) - private fun processZapAmountFromResponse( + private suspend fun processZapAmountFromResponse( nwcRequest: LnZapPaymentRequestEvent, nwcResponse: LnZapPaymentResponseEvent, - continuation: Continuation, - signer: NostrSigner, - ) { + signerState: NwcSignerState, + ): InvoiceAmount? { // if we can decrypt the reply - nwcResponse.response(signer) { noteEvent -> + return signerState.decryptResponse(nwcResponse)?.let { noteEvent -> // if it is a sucess if (noteEvent is PayInvoiceSuccessResponse) { // if we can decrypt the invoice - nwcRequest.lnInvoice(signer) { invoice -> + val request = signerState.decryptRequest(nwcRequest) + val invoice = (request as? PayInvoiceMethod)?.params?.invoice + if (invoice != null) { // if we can parse the amount val amount = try { @@ -686,34 +665,32 @@ open class Note( // avoid double counting if (amount != null) { - continuation.resume(InvoiceAmount(invoice, amount)) + InvoiceAmount(invoice, amount) } else { - continuation.resume(null) + null } + } else { + null } } else { - continuation.resume(null) + null } } } - suspend fun zappedAmountWithNWCPayments( - signer: NostrSigner, - onReady: (BigDecimal) -> Unit, - ) { + suspend fun zappedAmountWithNWCPayments(signerState: NwcSignerState): BigDecimal { if (zapPayments.isEmpty()) { - onReady(zapsAmount) + return zapsAmount } val invoiceSet = LinkedHashSet(zaps.size + zapPayments.size) zaps.forEach { (it.value?.event as? LnZapEvent)?.lnInvoice()?.let { invoiceSet.add(it) } } - zappedAmountCalculation( + return zappedAmountCalculation( zapsAmount, invoiceSet, zapPayments.toList(), - signer, - onReady, + signerState, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 2e7b27ece0..414abd6bbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.quartz.lightning.Lud06 import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -35,11 +34,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub -import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -47,7 +44,6 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.containsAny -import kotlinx.collections.immutable.persistentSetOf import kotlinx.coroutines.flow.MutableStateFlow import java.math.BigDecimal @@ -60,7 +56,6 @@ class User( var latestMetadata: MetadataEvent? = null var latestMetadataRelay: NormalizedRelayUrl? = null var latestContactList: ContactListEvent? = null - var latestBookmarkList: BookmarkListEvent? = null var reports = mapOf>() private set @@ -71,9 +66,6 @@ class User( var relaysBeingUsed = mapOf() private set - var privateChatrooms = mapOf() - private set - fun pubkey() = Hex.decode(pubkeyHex) fun pubkeyNpub() = pubkey().toNpub() @@ -124,13 +116,6 @@ class User( fun profilePicture(): String? = info?.picture - fun updateBookmark(event: BookmarkListEvent) { - if (event.id == latestBookmarkList?.id) return - - latestBookmarkList = event - flowSet?.bookmarks?.invalidateData() - } - fun updateContactList(event: ContactListEvent) { if (event.id == latestContactList?.id) return @@ -230,73 +215,6 @@ class User( } }.flatten() - @Synchronized - private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom = - privateChatrooms[key] - ?: run { - val privateChatroom = Chatroom() - privateChatrooms = privateChatrooms + Pair(key, privateChatroom) - privateChatroom - } - - private fun getOrCreatePrivateChatroom(user: User): Chatroom { - val key = ChatroomKey(persistentSetOf(user.pubkeyHex)) - return getOrCreatePrivateChatroom(key) - } - - private fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom = privateChatrooms[key] ?: getOrCreatePrivateChatroomSync(key) - - fun addMessage( - room: ChatroomKey, - msg: Note, - ) { - val privateChatroom = getOrCreatePrivateChatroom(room) - if (msg !in privateChatroom.roomMessages) { - privateChatroom.addMessageSync(msg) - flowSet?.messages?.invalidateData() - } - } - - fun addMessage( - user: User, - msg: Note, - ) { - val privateChatroom = getOrCreatePrivateChatroom(user) - if (msg !in privateChatroom.roomMessages) { - privateChatroom.addMessageSync(msg) - flowSet?.messages?.invalidateData() - } - } - - fun createChatroom(withKey: ChatroomKey) { - getOrCreatePrivateChatroom(withKey) - } - - fun removeMessage( - user: User, - msg: Note, - ) { - checkNotInMainThread() - - val privateChatroom = getOrCreatePrivateChatroom(user) - if (msg in privateChatroom.roomMessages) { - privateChatroom.removeMessageSync(msg) - flowSet?.messages?.invalidateData() - } - } - - fun removeMessage( - room: ChatroomKey, - msg: Note, - ) { - checkNotInMainThread() - val privateChatroom = getOrCreatePrivateChatroom(room) - if (msg in privateChatroom.roomMessages) { - privateChatroom.removeMessageSync(msg) - flowSet?.messages?.invalidateData() - } - } - fun addRelayBeingUsed( relay: NormalizedRelayUrl, eventTime: Long, @@ -343,12 +261,6 @@ class User( suspend fun transientFollowerCount(): Int = LocalCache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkeyHex) ?: false } - fun hasSentMessagesTo(key: ChatroomKey?): Boolean { - val messagesToUser = privateChatrooms[key] ?: return false - - return messagesToUser.authors.any { this == it } - } - fun hasReport( loggedIn: User, type: ReportType, @@ -432,10 +344,8 @@ class UserFlowSet( val relays = UserBundledRefresherFlow(u) val followers = UserBundledRefresherFlow(u) val reports = UserBundledRefresherFlow(u) - val messages = UserBundledRefresherFlow(u) val relayInfo = UserBundledRefresherFlow(u) val zaps = UserBundledRefresherFlow(u) - val bookmarks = UserBundledRefresherFlow(u) val statuses = UserBundledRefresherFlow(u) fun isInUse(): Boolean = @@ -444,10 +354,8 @@ class UserFlowSet( follows.hasObservers() || followers.hasObservers() || reports.hasObservers() || - messages.hasObservers() || relayInfo.hasObservers() || zaps.hasObservers() || - bookmarks.hasObservers() || statuses.hasObservers() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt new file mode 100644 index 0000000000..ca8e735495 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListDecryptionCache.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.edits + +import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent + +class PrivateStorageRelayListDecryptionCache( + signer: NostrSigner, +) : GenericRelayListCache(signer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt index 43af1fdf90..e4f28c081d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt @@ -26,9 +26,9 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -44,6 +44,7 @@ import kotlinx.coroutines.launch class PrivateStorageRelayListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: PrivateStorageRelayListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { @@ -55,9 +56,9 @@ class PrivateStorageRelayListState( fun getPrivateOutboxRelayList(): PrivateOutboxRelayListEvent? = getPrivateOutboxRelayListNote().event as? PrivateOutboxRelayListEvent - fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set { + suspend fun normalizePrivateOutboxRelayListWithBackup(note: Note): Set { val event = note.event as? PrivateOutboxRelayListEvent ?: settings.backupPrivateHomeRelayList - return event?.relays()?.toSet() ?: emptySet() + return event?.let { decryptionCache.relays(it) } ?: emptySet() } val flow = @@ -69,28 +70,22 @@ class PrivateStorageRelayListState( .stateIn( scope, SharingStarted.Eagerly, - normalizePrivateOutboxRelayListWithBackup(getPrivateOutboxRelayListNote()), + emptySet(), ) - fun saveRelayList( - relays: List, - onDone: (PrivateOutboxRelayListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun saveRelayList(relays: List): PrivateOutboxRelayListEvent { val relayListForPrivateOutbox = getPrivateOutboxRelayList() - if (relayListForPrivateOutbox != null && !relayListForPrivateOutbox.cachedPrivateTags().isNullOrEmpty()) { + return if (relayListForPrivateOutbox != null) { PrivateOutboxRelayListEvent.updateRelayList( earlierVersion = relayListForPrivateOutbox, relays = relays, signer = signer, - onReady = onDone, ) } else { - PrivateOutboxRelayListEvent.createFromScratch( + PrivateOutboxRelayListEvent.create( relays = relays, signer = signer, - onReady = onDone, ) } } @@ -100,17 +95,15 @@ class PrivateStorageRelayListState( Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } + LocalCache.justConsumeMyOwnEvent(event) } } scope.launch(Dispatchers.Default) { Log.d("AccountRegisterObservers", "Private Home Relay List Collector Start") - getPrivateOutboxRelayListFlow().collect { + getPrivateOutboxRelayListFlow().collect { noteState -> Log.d("AccountRegisterObservers", "Updating Private Home Relay List for ${signer.pubKey}") - (it.note.event as? PrivateOutboxRelayListEvent)?.let { + (noteState.note.event as? PrivateOutboxRelayListEvent)?.let { settings.updatePrivateHomeRelayList(it) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt similarity index 53% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt index 1396e3fd11..60ff2f80fc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/LoginResultProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt @@ -18,23 +18,24 @@ * 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.nip55AndroidSigner.api.foreground.processors +package com.vitorpamplona.amethyst.model.emphChat -import android.content.Intent -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.LoginResponse +import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent +import com.vitorpamplona.quartz.experimental.ephemChat.list.roomSet +import com.vitorpamplona.quartz.experimental.ephemChat.list.rooms +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache -class LoginResultProcessor( - val onReady: (HexKey, String) -> Unit, -) : NewResultProcessor { - override fun consume(intent: Intent) { - val foregroundResult = LoginResponse.parse(intent) +class EphemeralChatListDecryptionCache( + val signer: NostrSigner, +) { + val cachedPrivateLists = PrivateTagArrayEventCache(signer) - if (foregroundResult is SignerResult.Successful) { - onReady(foregroundResult.result.pubkey, foregroundResult.result.packageName) - } - } + fun cachedRoomSet(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagListPrecached(event).roomSet() + + fun cachedRooms(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagListPrecached(event).rooms() + + suspend fun roomSet(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagList(event).roomSet() + + suspend fun rooms(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagList(event).rooms() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt index 07d22c6581..ebc38b8dc2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt @@ -30,7 +30,6 @@ import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -43,11 +42,11 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import kotlin.coroutines.resume class EphemeralChatListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: EphemeralChatListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { @@ -59,17 +58,10 @@ class EphemeralChatListState( fun getEphemeralChatList(): EphemeralChatListEvent? = getEphemeralChatListNote().event as? EphemeralChatListEvent - suspend fun ephemeralChatListWithBackup(note: Note): Set = - ephemeralChatList( - note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList, - ) - - suspend fun ephemeralChatList(event: EphemeralChatListEvent?): Set = - tryAndWait { continuation -> - event?.publicAndPrivateRoomIds(signer) { - continuation.resume(it) - } - } ?: emptySet() + suspend fun ephemeralChatListWithBackup(note: Note): Set { + val event = note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList + return event?.let { decryptionCache.roomSet(it) } ?: emptySet() + } @OptIn(ExperimentalCoroutinesApi::class) val liveEphemeralChatList: StateFlow> = @@ -85,46 +77,36 @@ class EphemeralChatListState( emptySet(), ) - fun follow( - channel: EphemeralChatChannel, - onDone: (EphemeralChatListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun follow(channel: EphemeralChatChannel): EphemeralChatListEvent { val ephemeralChatList = getEphemeralChatList() - if (ephemeralChatList == null) { - EphemeralChatListEvent.createRoom( + return if (ephemeralChatList == null) { + EphemeralChatListEvent.create( room = channel.roomId, isPrivate = true, signer = signer, - onReady = onDone, ) } else { - EphemeralChatListEvent.addRoom( + EphemeralChatListEvent.add( earlierVersion = ephemeralChatList, room = channel.roomId, isPrivate = true, signer = signer, - onReady = onDone, ) } } - fun unfollow( - channel: EphemeralChatChannel, - onDone: (EphemeralChatListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun unfollow(channel: EphemeralChatChannel): EphemeralChatListEvent? { val ephemeralChatList = getEphemeralChatList() - if (ephemeralChatList != null) { - EphemeralChatListEvent.removeRoom( + return if (ephemeralChatList != null) { + EphemeralChatListEvent.remove( earlierVersion = ephemeralChatList, room = channel.roomId, - isPrivate = true, signer = signer, - onReady = onDone, ) + } else { + null } } @@ -133,17 +115,15 @@ class EphemeralChatListState( Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } + LocalCache.justConsumeMyOwnEvent(event) } } scope.launch(Dispatchers.Default) { Log.d("AccountRegisterObservers", "EphemeralChatList Collector Start") - getEphemeralChatListFlow().collect { + getEphemeralChatListFlow().collect { noteState -> Log.d("AccountRegisterObservers", "EphemeralChatList List for ${signer.pubKey}") - (it.note.event as? EphemeralChatListEvent)?.let { + (noteState.note.event as? EphemeralChatListEvent)?.let { settings.updateEphemeralChatListTo(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt index dc4e9a2c96..0061c0385f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt @@ -48,7 +48,7 @@ class UserMetadataState( fun getUserMetadataEvent(): MetadataEvent? = getUserMetadataUser().latestMetadata - fun sendNewUserMetadata( + suspend fun sendNewUserMetadata( name: String? = null, picture: String? = null, banner: String? = null, @@ -61,8 +61,7 @@ class UserMetadataState( twitter: String? = null, mastodon: String? = null, github: String? = null, - onDone: (MetadataEvent) -> Unit, - ) { + ): MetadataEvent { val latest = getUserMetadataEvent() val template = @@ -101,7 +100,7 @@ class UserMetadataState( ) } - signer.sign(template, onDone) + return signer.sign(template) } init { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt index bf6a4e37ab..cb577d294e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt index 59bedaa1e4..409f31e154 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListState.kt @@ -113,39 +113,31 @@ class FollowListState( ) } - fun follow( - user: User, - onDone: (ContactListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun follow(user: User): ContactListEvent { val contactList = getFollowListEvent() - if (contactList != null) { - ContactListEvent.followUser(contactList, user.pubkeyHex, signer, onReady = onDone) + return if (contactList != null) { + ContactListEvent.followUser(contactList, user.pubkeyHex, signer) } else { ContactListEvent.createFromScratch( followUsers = listOf(ContactTag(user.pubkeyHex, user.bestRelayHint(), null)), relayUse = emptyMap(), signer = signer, - onReady = onDone, ) } } - fun unfollow( - user: User, - onDone: (ContactListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun unfollow(user: User): ContactListEvent? { val contactList = getFollowListEvent() - if (contactList != null && contactList.tags.isNotEmpty()) { + return if (contactList != null && contactList.tags.isNotEmpty()) { ContactListEvent.unfollowUser( contactList, user.pubkeyHex, signer, - onReady = onDone, ) + } else { + null } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt index 00603939a0..39ac9db840 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model.nip02FollowLists import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.nip51Lists.BlockedRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt index d7be6fa974..08e3b4111a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt @@ -71,25 +71,18 @@ class DmRelayListState( emptySet(), ) - fun saveRelayList( - dmRelays: List, - onDone: (ChatMessageRelayListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return - + suspend fun saveRelayList(dmRelays: List): ChatMessageRelayListEvent { val relayListForDMs = getDMRelayList() - if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) { + return if (relayListForDMs != null && relayListForDMs.tags.isNotEmpty()) { ChatMessageRelayListEvent.updateRelayList( earlierVersion = relayListForDMs, relays = dmRelays, signer = signer, - onReady = onDone, ) } else { - ChatMessageRelayListEvent.createFromScratch( + ChatMessageRelayListEvent.create( relays = dmRelays, signer = signer, - onReady = onDone, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt index 54271181b2..4d5d1276d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip18Reposts/RepostAction.kt @@ -28,17 +28,15 @@ import com.vitorpamplona.quartz.nip18Reposts.RepostEvent class RepostAction { companion object { - fun repost( + suspend fun repost( note: Note, signer: NostrSigner, - onDone: (Event) -> Unit, - ) { - if (!signer.isWriteable()) return - val noteEvent = note.event ?: return + ): Event? { + val noteEvent = note.event ?: return null if (note.hasBoostedInTheLast5Minutes(signer.pubKey)) { // has already bosted in the past 5mins - return + return null } val noteHint = note.relayHintUrl() @@ -51,7 +49,7 @@ class RepostAction { GenericRepostEvent.build(noteEvent, noteHint, authorHint) } - signer.sign(template, onDone) + return signer.sign(template) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt index 9174c13d3d..bf580d23c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip25Reactions/ReactionAction.kt @@ -38,7 +38,7 @@ class ReactionAction { by: User, signer: NostrSigner, onPublic: (ReactionEvent) -> Unit, - onPrivate: (NIP17Factory.Result) -> Unit, + onPrivate: suspend (NIP17Factory.Result) -> Unit, ) { if (!signer.isWriteable()) return @@ -55,14 +55,14 @@ class ReactionAction { val emojiUrl = EmojiUrlTag.decode(reaction) if (emojiUrl != null) { note.toEventHint()?.let { - NIP17Factory().createReactionWithinGroup( - emojiUrl = emojiUrl, - originalNote = it, - to = users, - signer = signer, - ) { - onPrivate(it) - } + onPrivate( + NIP17Factory().createReactionWithinGroup( + emojiUrl = emojiUrl, + originalNote = it, + to = users, + signer = signer, + ), + ) } return @@ -70,14 +70,14 @@ class ReactionAction { } note.toEventHint()?.let { - NIP17Factory().createReactionWithinGroup( - content = reaction, - originalNote = it, - to = users, - signer = signer, - ) { - onPrivate(it) - } + onPrivate( + NIP17Factory().createReactionWithinGroup( + content = reaction, + originalNote = it, + to = users, + signer = signer, + ), + ) } return } else { @@ -87,10 +87,7 @@ class ReactionAction { note.event?.let { val template = ReactionEvent.build(emojiUrl, EventHintBundle(it, note.relayHintUrl())) - signer.sign( - template, - onReady = onPublic, - ) + onPublic(signer.sign(template)) } return @@ -98,10 +95,7 @@ class ReactionAction { } note.toEventHint()?.let { - signer.sign( - ReactionEvent.build(reaction, it), - onReady = onPublic, - ) + onPublic(signer.sign(ReactionEvent.build(reaction, it))) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt similarity index 53% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt index 8f8cdd68e0..a0a54ff93d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DecryptZapResultProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt @@ -18,23 +18,24 @@ * 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.nip55AndroidSigner.api.foreground.processors +package com.vitorpamplona.amethyst.model.nip28PublicChats -import android.content.Intent -import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DecryptZapResponse -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.channelSet +import com.vitorpamplona.quartz.nip28PublicChat.list.channels +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache -class DecryptZapResultProcessor( - val onReady: (LnZapPrivateEvent) -> Unit, -) : NewResultProcessor { - override fun consume(intent: Intent) { - val foregroundResult = DecryptZapResponse.parse(intent) +class PublicChatListDecryptionCache( + val signer: NostrSigner, +) { + val cachedPrivateLists = PrivateTagArrayEventCache(signer) - if (foregroundResult is SignerResult.Successful) { - onReady(foregroundResult.result.privateEvent) - } - } + fun cachedChannelSet(event: ChannelListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channelSet() + + fun cachedChannels(event: ChannelListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channels() + + suspend fun channelSet(event: ChannelListEvent) = cachedPrivateLists.mergeTagList(event).channelSet() + + suspend fun channels(event: ChannelListEvent) = cachedPrivateLists.mergeTagList(event).channels() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt index 109193c0ac..16218792a0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt @@ -28,10 +28,9 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent -import com.vitorpamplona.quartz.utils.tryAndWait +import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -45,11 +44,11 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import kotlin.coroutines.resume class PublicChatListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: PublicChatListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { @@ -61,20 +60,13 @@ class PublicChatListState( fun getChannelList(): ChannelListEvent? = getChannelListNote().event as? ChannelListEvent - suspend fun publicChatListWithBackup(note: Note): Set = - publicChatList( - note.event as? ChannelListEvent ?: settings.backupChannelList, - ) - - suspend fun publicChatList(event: ChannelListEvent?): Set = - tryAndWait { continuation -> - event?.publicAndPrivateChannels(signer) { - continuation.resume(it) - } - } ?: emptySet() + suspend fun publicChatListWithBackup(note: Note): Set { + val event = note.event as? ChannelListEvent ?: settings.backupChannelList + return event?.let { decryptionCache.channelSet(it) } ?: emptySet() + } @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow> = + val flow: StateFlow> = getChannelListFlow() .transformLatest { noteState -> emit(publicChatListWithBackup(noteState.note)) @@ -101,54 +93,34 @@ class PublicChatListState( emptySet(), ) - fun follow( - channel: PublicChatChannel, - onDone: (ChannelListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun follow(channel: PublicChatChannel): ChannelListEvent { val publicChatList = getChannelList() - val fullHint = channel.toEventHint() - if (fullHint != null) { - if (publicChatList == null) { - ChannelListEvent.createChannel(fullHint, true, signer, onReady = onDone) - } else { - ChannelListEvent.addChannel(publicChatList, fullHint, true, signer, onReady = onDone) - } + return if (publicChatList == null) { + ChannelListEvent.create(ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer) } else { - val partialHint = channel.toEventId() - if (publicChatList == null) { - ChannelListEvent.createChannel(partialHint, true, signer, onReady = onDone) - } else { - ChannelListEvent.addChannel(publicChatList, partialHint, true, signer, onReady = onDone) - } + ChannelListEvent.add(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer) } } - fun follow( - channels: List, - onDone: (ChannelListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun follow(channels: List): ChannelListEvent { val publicChatList = getChannelList() - val partialHint = channels.map { it.toEventId() } - if (publicChatList == null) { - ChannelListEvent.createChannels(partialHint, true, signer, onReady = onDone) + val channelTags = channels.map { ChannelTag(it.idHex, it.relayHintUrl()) } + return if (publicChatList == null) { + ChannelListEvent.create(channelTags, true, signer) } else { - ChannelListEvent.addChannels(publicChatList, partialHint, true, signer, onReady = onDone) + ChannelListEvent.add(publicChatList, channelTags, true, signer) } } - fun unfollow( - channel: PublicChatChannel, - onDone: (ChannelListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun unfollow(channel: PublicChatChannel): ChannelListEvent? { val publicChatList = getChannelList() - if (publicChatList != null) { - ChannelListEvent.removeChannel(publicChatList, channel.idHex, signer, onReady = onDone) + return if (publicChatList != null) { + ChannelListEvent.remove(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), signer) + } else { + null } } @@ -157,9 +129,7 @@ class PublicChatListState( Log.d("AccountRegisterObservers", "Loading saved channel list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } + LocalCache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt index e81567ae9d..b40392a63d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt @@ -127,39 +127,29 @@ class EmojiPackState( emptyList(), ) - fun addEmojiPack( - emojiPack: Note, - onDone: (EmojiPackSelectionEvent) -> Unit, - ) { - if (!signer.isWriteable()) return - + suspend fun addEmojiPack(emojiPack: Note): EmojiPackSelectionEvent { val emojiPackEvent = emojiPack.event - if (emojiPackEvent !is EmojiPackEvent) return + if (emojiPackEvent !is EmojiPackEvent) throw IllegalArgumentException("Cannot add an emoji pack to this kind of event.") - val eventHint = emojiPack.toEventHint() ?: return + val eventHint = emojiPack.toEventHint() ?: throw IllegalArgumentException("Cannot add an emoji pack to this kind of event.") val usersEmojiList = getEmojiPackSelection() - if (usersEmojiList == null) { + return if (usersEmojiList == null) { val template = EmojiPackSelectionEvent.build(listOf(eventHint)) - signer.sign(template, onDone) + signer.sign(template) } else { val template = EmojiPackSelectionEvent.add(usersEmojiList, eventHint) - signer.sign(template, onDone) + signer.sign(template) } } - fun removeEmojiPack( - emojiPack: Note, - onDone: (EmojiPackSelectionEvent) -> Unit, - ) { - if (!signer.isWriteable()) return - - val usersEmojiList = getEmojiPackSelection() ?: return + suspend fun removeEmojiPack(emojiPack: Note): EmojiPackSelectionEvent? { + val usersEmojiList = getEmojiPackSelection() ?: throw IllegalArgumentException("Cannot remove an emoji pack to this kind of event.") val emojiPackEvent = emojiPack.event - if (emojiPackEvent !is EmojiPackEvent) return + if (emojiPackEvent !is EmojiPackEvent) return null val template = EmojiPackSelectionEvent.remove(usersEmojiList, emojiPackEvent) - signer.sign(template, onDone) + return signer.sign(template) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt index 645d8a2c8b..834e2437f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt @@ -28,44 +28,35 @@ import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent class UserStatusAction { companion object { - fun create( + suspend fun create( newStatus: String, signer: NostrSigner, - onDone: (StatusEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + ): StatusEvent = StatusEvent.create(newStatus, "general", expiration = null, signer) - StatusEvent.create(newStatus, "general", expiration = null, signer, onReady = onDone) - } - - fun update( + suspend fun update( oldStatus: AddressableNote, newStatus: String, signer: NostrSigner, - onDone: (StatusEvent) -> Unit, - ) { - if (!signer.isWriteable()) return - val oldEvent = oldStatus.event as? StatusEvent ?: return + ): StatusEvent { + val oldEvent = oldStatus.event as? StatusEvent ?: throw IllegalStateException("Tried to update a non-status event") - StatusEvent.update(oldEvent, newStatus, signer, onReady = onDone) + return StatusEvent.update(oldEvent, newStatus, signer) } - fun delete( + suspend fun delete( oldStatus: AddressableNote, signer: NostrSigner, - onDone: (Event) -> Unit, - ) { - if (!signer.isWriteable()) return - val oldEvent = oldStatus.event as? StatusEvent ?: return + ): List { + val oldEvent = oldStatus.event as? StatusEvent ?: throw IllegalStateException("Tried to update a non-status event") - StatusEvent.clear(oldEvent, signer) { event -> - onDone(event) + val event = StatusEvent.clear(oldEvent, signer) + val deletion = signer.sign( DeletionEvent.buildForVersionOnly(listOf(event)), - onReady = onDone, ) - } + + return listOf(event, deletion) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt new file mode 100644 index 0000000000..9444bedbab --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip47WalletConnect + +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentQueryState +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent +import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect +import com.vitorpamplona.quartz.nip47WalletConnect.NostrWalletConnectRequestCache +import com.vitorpamplona.quartz.nip47WalletConnect.NostrWalletConnectResponseCache +import com.vitorpamplona.quartz.nip47WalletConnect.Request +import com.vitorpamplona.quartz.nip47WalletConnect.Response +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch + +/** + * Manages NIP-47 (Nostr Wallet Connect) related signing operations and decryption cache for a given account. + * + * Key Responsibilities: + * + * - Dynamically creates a NIP-47 signer if the wallet setup changes in the account settings. + * - Provides decryption caches to manage decrypted NIP-47 requests and responses efficiently. + * - Handles creating of zap payment requests and waits for responses. + * + * @property signer the main Nostr signer used for general Nostr operations + * @property cache the local cache for handling notes and events + * @property scope the coroutine scope used for async operations + * @property settings the account settings containing NIP-47 configuration + */ +class NwcSignerState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + /** + * Derives a NIP-47 signer from the zap payment request in settings. + * If there's no valid configuration, it defaults to the main signer. + * Flows updates whenever settings change. + */ + val nip47Signer = + settings.zapPaymentRequest + .map { + buildSigner(it) ?: signer + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + buildSigner(settings.zapPaymentRequest.value) ?: signer, + ) + + /** + * Creates a dedicated request decryption cache for the NIP-47 signer. + * Flows updates whenever the signer changes. + */ + val zapPaymentRequestDecryptionCache = + nip47Signer + .map { + NostrWalletConnectRequestCache(it) + }.flowOn(Dispatchers.Default) + .stateIn(scope, SharingStarted.Eagerly, NostrWalletConnectRequestCache(nip47Signer.value)) + + /** + * Creates a dedicated response decryption cache for the NIP-47 signer. + * Flows updates whenever the signer changes. + */ + val zapPaymentResponseDecryptionCache = + nip47Signer + .map { + NostrWalletConnectResponseCache(it) + }.flowOn(Dispatchers.Default) + .stateIn(scope, SharingStarted.Eagerly, NostrWalletConnectResponseCache(nip47Signer.value)) + + fun buildSigner(uri: Nip47WalletConnect.Nip47URINorm?) = + uri?.secret?.hexToByteArray()?.let { + NostrSignerInternal(KeyPair(it)) + } + + fun hasWalletConnectSetup(): Boolean = settings.zapPaymentRequest.value != null + + fun isNIP47Author(pubkeyHex: String?): Boolean = nip47Signer.value.pubKey == pubkeyHex + + /** + * Decrypts a NIP-47 payment request using the current signer. + * + * @param nwcRequest the NIP-47 payment request event to decrypt + * @return the decrypted request or null if not set up or decryption fails + */ + suspend fun decryptRequest(nwcRequest: LnZapPaymentRequestEvent): Request? { + if (!hasWalletConnectSetup()) return null + return zapPaymentRequestDecryptionCache.value.decryptRequest(nwcRequest) + } + + /** + * Decrypts a NIP-47 payment response using the current signer. + * + * @param nwsResponse the NIP-47 payment response event to decrypt + * @return the decrypted response or null if not set up or decryption fails + */ + suspend fun decryptResponse(nwsResponse: LnZapPaymentResponseEvent): Response? { + if (!hasWalletConnectSetup()) return null + return zapPaymentResponseDecryptionCache.value.decryptResponse(nwsResponse) + } + + /** + * Sends a zap payment request to a connected Lightning wallet. + * Subscribes to responses and waits up to 60s for a reply. + * + * @param bolt11 the BOLT-11 invoice to pay + * @param zappedNote the note being zapped (if any) + * @param onResponse callback to handle the response from the wallet + * @return a pair containing the payment request event and target relay URL + * @throws IllegalArgumentException if no NIP-47 wallet is set up + */ + suspend fun sendZapPaymentRequestFor( + bolt11: String, + zappedNote: Note?, + onResponse: (Response?) -> Unit, + ): Pair { + val walletService = settings.zapPaymentRequest.value + if (walletService == null) throw IllegalArgumentException("No NIP47 setup") + + val event = LnZapPaymentRequestEvent.create(bolt11, walletService.pubKeyHex, signer) + + val filter = + NWCPaymentQueryState( + fromServiceHex = walletService.pubKeyHex, + toUserHex = event.pubKey, + replyingToHex = event.id, + relay = walletService.relayUri, + ) + + Amethyst.instance.sources.nwc + .subscribe(filter) + + scope.launch(Dispatchers.IO) { + delay(60000) // waits 1 minute to complete payment. + Amethyst.instance.sources.nwc + .unsubscribe(filter) + } + + cache.consume(event, zappedNote, true, walletService.relayUri) { + onResponse(decryptResponse(it)) + } + + return Pair(event, walletService.relayUri) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt new file mode 100644 index 0000000000..57069ef61b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BookmarkListState.kt @@ -0,0 +1,276 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists + +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class BookmarkListState( + val signer: NostrSigner, + val cache: LocalCache, + val scope: CoroutineScope, +) { + class BookmarkList( + val public: List = emptyList(), + val private: List = emptyList(), + ) + + fun getBookmarkListAddress() = BookmarkListEvent.createBookmarkAddress(signer.pubKey) + + fun getBookmarkListNote() = cache.getOrCreateAddressableNote(getBookmarkListAddress()) + + fun getBookmarkListFlow(): StateFlow = getBookmarkListNote().flow().metadata.stateFlow + + fun getBookmarkList(): BookmarkListEvent? = getBookmarkListNote().event as? BookmarkListEvent + + suspend fun publicBookmarks(note: Note): List { + val noteEvent = note.event as? BookmarkListEvent + return noteEvent?.publicBookmarks() ?: emptyList() + } + + suspend fun privateBookmarks(note: Note): List { + val noteEvent = note.event as? BookmarkListEvent + return noteEvent?.privateBookmarks(signer) ?: emptyList() + } + + @OptIn(FlowPreview::class) + val publicBookmarks: StateFlow> = + getBookmarkListFlow() + .map { noteState -> + publicBookmarks(noteState.note) + }.onStart { + emit(publicBookmarks(getBookmarkListNote())) + }.debounce(100) + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + + @OptIn(FlowPreview::class) + val privateBookmarks: StateFlow> = + getBookmarkListFlow() + .map { noteState -> + privateBookmarks(noteState.note) + }.onStart { + emit(privateBookmarks(getBookmarkListNote())) + }.debounce(100) + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + + val publicBookmarkEventIdSet = + publicBookmarks + .map { bookmark -> + bookmark + .mapNotNull { + if (it is EventBookmark) it.eventId else null + }.toSet() + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + + val publicBookmarkAddressIdSet = + publicBookmarks + .map { bookmark -> + bookmark + .mapNotNull { + if (it is AddressBookmark) it.address else null + }.toSet() + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + + val privateBookmarkEventIdSet = + privateBookmarks + .map { bookmark -> + bookmark + .mapNotNull { + if (it is EventBookmark) it.eventId else null + }.toSet() + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + + val privateBookmarkAddressIdSet = + privateBookmarks + .map { bookmark -> + bookmark + .mapNotNull { + if (it is AddressBookmark) it.address else null + }.toSet() + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + + fun bookmarkList( + privateBookmarks: List, + publicBookmarks: List, + ): BookmarkList = + BookmarkList( + public = + publicBookmarks + .mapNotNull { + when (it) { + is EventBookmark -> cache.checkGetOrCreateNote(it.eventId) + is AddressBookmark -> cache.getOrCreateAddressableNote(it.address) + } + }.reversed(), + private = + privateBookmarks + .mapNotNull { + when (it) { + is EventBookmark -> cache.checkGetOrCreateNote(it.eventId) + is AddressBookmark -> cache.getOrCreateAddressableNote(it.address) + } + }.reversed(), + ) + + @OptIn(FlowPreview::class) + val bookmarks: StateFlow = + combineTransform(privateBookmarks, publicBookmarks) { private, public -> + emit(bookmarkList(private, public)) + }.onStart { + emit(bookmarkList(privateBookmarks.value, publicBookmarks.value)) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + BookmarkList(), + ) + + fun isInPrivateBookmarks(note: Note): Boolean { + if (!signer.isWriteable()) return false + + return if (note is AddressableNote) { + privateBookmarkAddressIdSet.value.contains(note.address) + } else { + privateBookmarkEventIdSet.value.contains(note.idHex) + } + } + + fun isInPublicBookmarks(note: Note): Boolean = + if (note is AddressableNote) { + publicBookmarkAddressIdSet.value.contains(note.address) + } else { + publicBookmarkEventIdSet.value.contains(note.idHex) + } + + suspend fun addBookmark( + note: Note, + isPrivate: Boolean, + ): BookmarkListEvent { + val bookmarkList = getBookmarkList() + + return if (bookmarkList == null) { + if (note is AddressableNote) { + BookmarkListEvent.create( + bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()), + isPrivate = isPrivate, + signer = signer, + ) + } else { + BookmarkListEvent.create( + bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()), + isPrivate = isPrivate, + signer = signer, + ) + } + } else { + if (note is AddressableNote) { + BookmarkListEvent.add( + earlierVersion = bookmarkList, + bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()), + isPrivate = isPrivate, + signer = signer, + ) + } else { + BookmarkListEvent.add( + earlierVersion = bookmarkList, + bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()), + isPrivate = isPrivate, + signer = signer, + ) + } + } + } + + suspend fun removeBookmark( + note: Note, + isPrivate: Boolean, + ): BookmarkListEvent? { + val bookmarkList = getBookmarkList() + + return if (bookmarkList != null) { + if (note is AddressableNote) { + BookmarkListEvent.remove( + earlierVersion = bookmarkList, + bookmarkIdTag = AddressBookmark(note.address, note.relayHintUrl()), + isPrivate = isPrivate, + signer = signer, + ) + } else { + BookmarkListEvent.remove( + earlierVersion = bookmarkList, + bookmarkIdTag = EventBookmark(note.idHex, note.relayHintUrl()), + isPrivate = isPrivate, + signer = signer, + ) + } + } else { + null + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt index d0fd40434b..ce359259b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HiddenUsersState.kt @@ -24,7 +24,9 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag import com.vitorpamplona.quartz.utils.DualCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -38,8 +40,8 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update class HiddenUsersState( - val muteList: StateFlow, - val blockList: StateFlow, + val muteList: StateFlow>, + val blockList: StateFlow>, val scope: CoroutineScope, val settings: AccountSettings, ) { @@ -61,14 +63,14 @@ class HiddenUsersState( } suspend fun assembleLiveHiddenUsers( - blockList: PeopleListEvent.UsersAndWords, - muteList: PeopleListEvent.UsersAndWords, + blockList: List, + muteList: List, transientHiddenUsers: Set, showSensitiveContent: Boolean?, ): LiveHiddenUsers = LiveHiddenUsers( - hiddenUsers = blockList.users + muteList.users, - hiddenWords = blockList.words + muteList.words, + hiddenUsers = blockList.mapNotNullTo(mutableSetOf()) { if (it is UserTag) it.pubKey else null } + muteList.mapNotNull { if (it is UserTag) it.pubKey else null }, + hiddenWords = blockList.mapNotNullTo(mutableSetOf()) { if (it is WordTag) it.word else null } + muteList.mapNotNull { if (it is WordTag) it.word else null }, spammers = transientHiddenUsers, showSensitiveContent = showSensitiveContent, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt similarity index 58% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt index e40ef0a77b..2bf5a11cb2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockPeopleListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/BlockPeopleListState.kt @@ -18,14 +18,16 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists +package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.utils.tryAndWait +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted @@ -34,11 +36,11 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlin.coroutines.resume class BlockPeopleListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: PeopleListDecryptionCache, val scope: CoroutineScope, ) { fun getBlockListAddress() = PeopleListEvent.createBlockAddress(signer.pubKey) @@ -49,17 +51,10 @@ class BlockPeopleListState( fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent - suspend fun blockListWithBackup(note: Note): PeopleListEvent.UsersAndWords = - blockList( - note.event as? PeopleListEvent, - ) - - suspend fun blockList(event: PeopleListEvent?): PeopleListEvent.UsersAndWords = - tryAndWait { continuation -> - event?.publicAndPrivateUsersAndWords(signer) { - continuation.resume(it) - } - } ?: PeopleListEvent.UsersAndWords() + suspend fun blockListWithBackup(note: Note): List { + val event = note.event as? PeopleListEvent + return event?.let { decryptionCache.usersAndWords(it) } ?: emptyList() + } val flow = getBlockListFlow() @@ -69,88 +64,55 @@ class BlockPeopleListState( .stateIn( scope, SharingStarted.Eagerly, - PeopleListEvent.UsersAndWords(), + emptyList(), ) - fun hideUser( - pubkeyHex: String, - onDone: (PeopleListEvent) -> Unit, - ) { + suspend fun hideUser(pubkeyHex: String): PeopleListEvent { val blockList = getBlockList() - if (blockList != null) { - PeopleListEvent.addUser( + return if (blockList != null) { + PeopleListEvent.add( earlierVersion = blockList, - pubKeyHex = pubkeyHex, + person = UserTag(pubkeyHex), isPrivate = true, signer = signer, - onReady = onDone, ) } else { - PeopleListEvent.createListWithUser( + PeopleListEvent.create( name = PeopleListEvent.BLOCK_LIST_D_TAG, - pubKeyHex = pubkeyHex, + person = UserTag(pubkeyHex), isPrivate = true, signer = signer, - onReady = onDone, + dTag = PeopleListEvent.BLOCK_LIST_D_TAG, ) } } - fun showUser( - pubkeyHex: String, - onDone: (PeopleListEvent) -> Unit, - ) { + suspend fun showUser(pubkeyHex: String): PeopleListEvent? { val blockList = getBlockList() - if (blockList != null) { - PeopleListEvent.removeUser( + return if (blockList != null) { + PeopleListEvent.remove( earlierVersion = blockList, - pubKeyHex = pubkeyHex, + person = UserTag(pubkeyHex), signer = signer, - onReady = onDone, - ) - } - } - - fun hideWord( - word: String, - onDone: (PeopleListEvent) -> Unit, - ) { - val blockList = getBlockList() - - if (blockList != null) { - PeopleListEvent.addWord( - earlierVersion = blockList, - word = word, - isPrivate = true, - signer = signer, - onReady = onDone, ) } else { - PeopleListEvent.createListWithWord( - name = PeopleListEvent.BLOCK_LIST_D_TAG, - word = word, - isPrivate = true, - signer = signer, - onReady = onDone, - ) + null } } - fun showWord( - word: String, - onDone: (PeopleListEvent) -> Unit, - ) { + suspend fun showWord(word: String): PeopleListEvent? { val blockList = getBlockList() - if (blockList != null) { - PeopleListEvent.removeWord( + return if (blockList != null) { + PeopleListEvent.remove( earlierVersion = blockList, - word = word, + person = WordTag(word), signer = signer, - onReady = onDone, ) + } else { + null } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/PeopleListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/PeopleListDecryptionCache.kt new file mode 100644 index 0000000000..23d5ed4b39 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockPeopleList/PeopleListDecryptionCache.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList + +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache +import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUserIdSet +import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsers +import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsersAndWords +import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWordSet +import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWords +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent + +class PeopleListDecryptionCache( + val signer: NostrSigner, +) { + val cachedPrivateLists = PrivateTagArrayEventCache(signer) + + fun cachedUsersAndWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsersAndWords() + + fun cachedUsers(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsers() + + fun cachedUserIdSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUserIdSet() + + fun cachedWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWords() + + fun cachedWordSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWordSet() + + suspend fun usersAndWords(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsersAndWords() + + suspend fun users(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsers() + + suspend fun userIdSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedUserIdSet() + + suspend fun words(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWords() + + suspend fun wordSet(event: PeopleListEvent) = cachedPrivateLists.mergeTagList(event).mutedWordSet() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt new file mode 100644 index 0000000000..a3f3df679f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListDecryptionCache.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays + +import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent + +class BlockedRelayListDecryptionCache( + signer: NostrSigner, +) : GenericRelayListCache(signer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt similarity index 80% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt index b897955254..237e892164 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/BlockedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists +package com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays import android.util.Log import com.vitorpamplona.amethyst.model.AccountSettings @@ -28,7 +28,8 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -44,10 +45,11 @@ import kotlinx.coroutines.launch class BlockedRelayListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: BlockedRelayListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { - fun getBlockedRelayListAddress() = BlockedRelayListEvent.createAddress(signer.pubKey) + fun getBlockedRelayListAddress() = BlockedRelayListEvent.Companion.createAddress(signer.pubKey) fun getBlockedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlockedRelayListAddress()) @@ -55,9 +57,9 @@ class BlockedRelayListState( fun getBlockedRelayList(): BlockedRelayListEvent? = getBlockedRelayListNote().event as? BlockedRelayListEvent - fun normalizeBlockedRelayListWithBackup(note: Note): Set { + suspend fun normalizeBlockedRelayListWithBackup(note: Note): Set { val event = note.event as? BlockedRelayListEvent ?: settings.backupBlockedRelayList - return event?.relays()?.toSet() ?: emptySet() + return event?.let { decryptionCache.relays(it) } ?: emptySet() } val flow = @@ -67,29 +69,24 @@ class BlockedRelayListState( .flowOn(Dispatchers.Default) .stateIn( scope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, emptySet(), ) - fun saveRelayList( - blockedRelays: List, - onDone: (BlockedRelayListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun saveRelayList(blockedRelays: List): BlockedRelayListEvent { + if (!signer.isWriteable()) throw SignerExceptions.ReadOnlyException() val relayListForBlocked = getBlockedRelayList() - if (relayListForBlocked != null && relayListForBlocked.tags.isNotEmpty()) { - BlockedRelayListEvent.updateRelayList( + return if (relayListForBlocked != null && relayListForBlocked.tags.isNotEmpty()) { + BlockedRelayListEvent.Companion.updateRelayList( earlierVersion = relayListForBlocked, relays = blockedRelays, signer = signer, - onReady = onDone, ) } else { - BlockedRelayListEvent.createFromScratch( + BlockedRelayListEvent.Companion.create( relays = blockedRelays, signer = signer, - onReady = onDone, ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt similarity index 86% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt index c7281842c7..25f818e726 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/NewResultProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListCard.kt @@ -18,10 +18,10 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground +package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists -import android.content.Intent +data class GeohashListCard( + val relays: List, +) -interface NewResultProcessor { - fun consume(intent: Intent) -} +val EmptyGeohashListCard = GeohashListCard(emptyList()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt new file mode 100644 index 0000000000..2576cbc123 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListDecryptionCache.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache +import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.geohashList.geohashSet +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onStart + +class GeohashListDecryptionCache( + val signer: NostrSigner, +) { + val cachedPrivateLists = PrivateTagArrayEventCache(signer) + + fun cachedGeohashes(event: GeohashListEvent) = cachedPrivateLists.mergeTagListPrecached(event).geohashSet() + + suspend fun geohashes(event: GeohashListEvent) = cachedPrivateLists.mergeTagList(event).geohashSet() + + fun fastStartValueForGeohashList(note: Note): GeohashListCard { + val noteEvent = note.event as? GeohashListEvent + return if (noteEvent != null) { + GeohashListCard(cachedGeohashes(noteEvent).toList()) + } else { + EmptyGeohashListCard + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun observeDecryptedGeohashList(note: Note): Flow = + note + .flow() + .metadata.stateFlow + .mapLatest { noteState -> + val event = noteState.note.event as? GeohashListEvent + GeohashListCard(event?.let { geohashes(it).toList() } ?: emptyList()) + }.onStart { + val event = note.event as? GeohashListEvent + if (event != null) { + val list = geohashes(event) + if (list.isNotEmpty()) { + emit(GeohashListCard(list.toList())) + } else { + emit(EmptyGeohashListCard) + } + } else { + emit(EmptyGeohashListCard) + } + }.distinctUntilChanged() + .flowOn(Dispatchers.Default) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt similarity index 67% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt index 096a55fca7..a11c07130b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/GeohashListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists +package com.vitorpamplona.amethyst.model.nip51Lists.geohashLists import android.util.Log import com.vitorpamplona.amethyst.model.AccountSettings @@ -27,8 +27,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent -import com.vitorpamplona.quartz.utils.tryAndWait +import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -41,11 +40,11 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import kotlin.coroutines.resume class GeohashListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: GeohashListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { @@ -57,17 +56,10 @@ class GeohashListState( fun getGeohashList(): GeohashListEvent? = getGeohashListNote().event as? GeohashListEvent - suspend fun geohashListWithBackup(note: Note): Set = - geohashList( - note.event as? GeohashListEvent ?: settings.backupGeohashList, - ) - - suspend fun geohashList(event: GeohashListEvent?): Set = - tryAndWait { continuation -> - event?.publicAndPrivateGeohash(signer) { - continuation.resume(it) - } - } ?: emptySet() + suspend fun geohashListWithBackup(note: Note): Set { + val event = note.event as? GeohashListEvent ?: settings.backupGeohashList + return event?.let { decryptionCache.geohashes(it) } ?: emptySet() + } @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow> = @@ -83,43 +75,33 @@ class GeohashListState( emptySet(), ) - fun follow( - geohashs: List, - onDone: (GeohashListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun follow(geohashes: List): GeohashListEvent { val geohashList = getGeohashList() - if (geohashList == null) { - GeohashListEvent.createGeohashs(geohashs, true, signer, onReady = onDone) + return if (geohashList == null) { + GeohashListEvent.create(geohashes, true, signer) } else { - GeohashListEvent.addGeohashs(geohashList, geohashs, true, signer, onReady = onDone) + GeohashListEvent.add(geohashList, geohashes, true, signer) } } - fun follow( - geohash: String, - onDone: (GeohashListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun follow(geohash: String): GeohashListEvent { val geohashList = getGeohashList() - if (geohashList == null) { - GeohashListEvent.createGeohash(geohash, true, signer, onReady = onDone) + return if (geohashList == null) { + GeohashListEvent.create(geohash, true, signer) } else { - GeohashListEvent.addGeohash(geohashList, geohash, true, signer, onReady = onDone) + GeohashListEvent.add(geohashList, geohash, true, signer) } } - fun unfollow( - geohash: String, - onDone: (GeohashListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun unfollow(geohash: String): GeohashListEvent? { val geohashList = getGeohashList() - if (geohashList != null) { - GeohashListEvent.removeGeohash(geohashList, geohash, signer, onReady = onDone) + return if (geohashList != null) { + GeohashListEvent.remove(geohashList, geohash, signer) + } else { + null } } @@ -128,17 +110,15 @@ class GeohashListState( Log.d("AccountRegisterObservers", "Loading saved Geohash list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } + LocalCache.justConsumeMyOwnEvent(event) } } scope.launch(Dispatchers.Default) { Log.d("AccountRegisterObservers", "Geohash List Collector Start") - getGeohashListFlow().collect { + getGeohashListFlow().collect { noteState -> Log.d("AccountRegisterObservers", "Geohash List for ${signer.pubKey}") - (it.note.event as? GeohashListEvent)?.let { + (noteState.note.event as? GeohashListEvent)?.let { settings.updateGeohashListTo(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt new file mode 100644 index 0000000000..6e73b068ae --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListDecryptionCache.kt @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists + +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.hashtagSet + +class HashtagListDecryptionCache( + val signer: NostrSigner, +) { + val cachedPrivateLists = PrivateTagArrayEventCache(signer) + + fun cachedHashtags(event: HashtagListEvent) = cachedPrivateLists.mergeTagListPrecached(event).hashtagSet() + + suspend fun hashtags(event: HashtagListEvent) = cachedPrivateLists.mergeTagList(event).hashtagSet() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt similarity index 67% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt index acf863a109..adf99d0378 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists +package com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists import android.util.Log import com.vitorpamplona.amethyst.model.AccountSettings @@ -27,8 +27,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent -import com.vitorpamplona.quartz.utils.tryAndWait +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -41,15 +40,15 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import kotlin.coroutines.resume class HashtagListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: HashtagListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { - fun getHashtagListAddress() = HashtagListEvent.createAddress(signer.pubKey) + fun getHashtagListAddress() = HashtagListEvent.Companion.createAddress(signer.pubKey) fun getHashtagListNote(): AddressableNote = cache.getOrCreateAddressableNote(getHashtagListAddress()) @@ -57,17 +56,10 @@ class HashtagListState( fun getHashtagList(): HashtagListEvent? = getHashtagListNote().event as? HashtagListEvent - suspend fun hashtagListWithBackup(note: Note): Set = - hashtagList( - note.event as? HashtagListEvent ?: settings.backupHashtagList, - ) - - suspend fun hashtagList(event: HashtagListEvent?): Set = - tryAndWait { continuation -> - event?.publicAndPrivateHashtag(signer) { - continuation.resume(it) - } - } ?: emptySet() + suspend fun hashtagListWithBackup(note: Note): Set { + val event = note.event as? HashtagListEvent ?: settings.backupHashtagList + return event?.let { decryptionCache.hashtags(it) } ?: emptySet() + } @OptIn(ExperimentalCoroutinesApi::class) val flow: StateFlow> = @@ -79,47 +71,37 @@ class HashtagListState( }.flowOn(Dispatchers.Default) .stateIn( scope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, emptySet(), ) - fun follow( - hashtags: List, - onDone: (HashtagListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun follow(hashtags: List): HashtagListEvent { val hashtagList = getHashtagList() - if (hashtagList == null) { - HashtagListEvent.createHashtags(hashtags, true, signer, onReady = onDone) + return if (hashtagList == null) { + HashtagListEvent.Companion.create(hashtags, true, signer) } else { - HashtagListEvent.addHashtags(hashtagList, hashtags, true, signer, onReady = onDone) + HashtagListEvent.Companion.add(hashtagList, hashtags, true, signer) } } - fun follow( - hashtag: String, - onDone: (HashtagListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun follow(hashtag: String): HashtagListEvent { val hashtagList = getHashtagList() - if (hashtagList == null) { - HashtagListEvent.createHashtag(hashtag, true, signer, onReady = onDone) + return if (hashtagList == null) { + HashtagListEvent.Companion.create(hashtag, true, signer) } else { - HashtagListEvent.addHashtag(hashtagList, hashtag, true, signer, onReady = onDone) + HashtagListEvent.Companion.add(hashtagList, hashtag, true, signer) } } - fun unfollow( - hashtag: String, - onDone: (HashtagListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun unfollow(hashtag: String): HashtagListEvent? { val hashtagList = getHashtagList() - if (hashtagList != null) { - HashtagListEvent.removeHashtag(hashtagList, hashtag, signer, onReady = onDone) + return if (hashtagList != null) { + HashtagListEvent.Companion.remove(hashtagList, hashtag, signer) + } else { + null } } @@ -128,9 +110,7 @@ class HashtagListState( Log.d("AccountRegisterObservers", "Loading saved Hashtag list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } + LocalCache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt new file mode 100644 index 0000000000..90314fc4a0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListDecryptionCache.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.muteList + +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUserIdSet +import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsers +import com.vitorpamplona.quartz.nip51Lists.muteList.mutedUsersAndWords +import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWordSet +import com.vitorpamplona.quartz.nip51Lists.muteList.mutedWords + +class MuteListDecryptionCache( + val signer: NostrSigner, +) { + val cachedPrivateLists = PrivateTagArrayEventCache(signer) + + fun cachedUsersAndWords(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsersAndWords() + + fun cachedUsers(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUsers() + + fun cachedUserIdSet(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedUserIdSet() + + fun cachedWords(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWords() + + fun cachedWordSet(event: MuteListEvent) = cachedPrivateLists.mergeTagListPrecached(event).mutedWordSet() + + suspend fun mutedUsersAndWords(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsersAndWords() + + suspend fun mutedUsers(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedUsers() + + suspend fun mutedUserIdSet(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedUserIdSet() + + suspend fun mutedWords(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedWords() + + suspend fun mutedWordSet(event: MuteListEvent) = cachedPrivateLists.mergeTagList(event).mutedWordSet() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt similarity index 67% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt index c79549def9..e284cc024a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/MuteListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists +package com.vitorpamplona.amethyst.model.nip51Lists.muteList import android.util.Log import com.vitorpamplona.amethyst.model.AccountSettings @@ -26,9 +26,10 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.utils.tryAndWait +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -40,11 +41,11 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlin.coroutines.resume class MuteListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: MuteListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { @@ -56,17 +57,10 @@ class MuteListState( fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent - suspend fun muteListWithBackup(note: Note): PeopleListEvent.UsersAndWords = - muteList( - note.event as? MuteListEvent ?: settings.backupMuteList, - ) - - suspend fun muteList(event: MuteListEvent?): PeopleListEvent.UsersAndWords = - tryAndWait { continuation -> - event?.publicAndPrivateUsersAndWords(signer) { - continuation.resume(it) - } - } ?: PeopleListEvent.UsersAndWords() + suspend fun muteListWithBackup(note: Note): List { + val event = note.event as? MuteListEvent ?: settings.backupMuteList + return event?.let { decryptionCache.mutedUsersAndWords(it) } ?: emptyList() + } val flow = getMuteListFlow() @@ -76,86 +70,72 @@ class MuteListState( .stateIn( scope, SharingStarted.Eagerly, - PeopleListEvent.UsersAndWords(), + emptyList(), ) - fun hideUser( - pubkeyHex: String, - onDone: (MuteListEvent) -> Unit, - ) { + suspend fun hideUser(pubkeyHex: String): MuteListEvent { val muteList = getMuteList() - if (muteList != null) { - MuteListEvent.addUser( + return if (muteList != null) { + MuteListEvent.add( earlierVersion = muteList, - pubKeyHex = pubkeyHex, + mute = UserTag(pubkeyHex), isPrivate = true, signer = signer, - onReady = onDone, ) } else { - MuteListEvent.createListWithUser( - pubKeyHex = pubkeyHex, + MuteListEvent.create( + mute = UserTag(pubkeyHex), isPrivate = true, signer = signer, - onReady = onDone, ) } } - fun showUser( - pubkeyHex: String, - onDone: (MuteListEvent) -> Unit, - ) { + suspend fun showUser(pubkeyHex: String): MuteListEvent? { val muteList = getMuteList() - if (muteList != null) { - MuteListEvent.removeUser( + return if (muteList != null) { + MuteListEvent.remove( earlierVersion = muteList, - pubKeyHex = pubkeyHex, + mute = UserTag(pubkeyHex), signer = signer, - onReady = onDone, - ) - } - } - - fun hideWord( - word: String, - onDone: (MuteListEvent) -> Unit, - ) { - val muteList = getMuteList() - - if (muteList != null) { - MuteListEvent.addWord( - earlierVersion = muteList, - word = word, - isPrivate = true, - signer = signer, - onReady = onDone, ) } else { - MuteListEvent.createListWithWord( - word = word, + null + } + } + + suspend fun hideWord(word: String): MuteListEvent { + val muteList = getMuteList() + + return if (muteList != null) { + MuteListEvent.add( + earlierVersion = muteList, + mute = WordTag(word), + isPrivate = true, + signer = signer, + ) + } else { + MuteListEvent.create( + mute = WordTag(word), isPrivate = true, signer = signer, - onReady = onDone, ) } } - fun showWord( - word: String, - onDone: (MuteListEvent) -> Unit, - ) { + suspend fun showWord(word: String): MuteListEvent? { val muteList = getMuteList() - if (muteList != null) { - MuteListEvent.removeWord( + return if (muteList != null) { + MuteListEvent.remove( earlierVersion = muteList, - word = word, + mute = WordTag(word), signer = signer, - onReady = onDone, ) + } else { + null } } @@ -164,9 +144,7 @@ class MuteListState( Log.d("AccountRegisterObservers", "Loading saved mute list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } + LocalCache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt new file mode 100644 index 0000000000..c8735fbda9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/GenericRelayListCache.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.relayLists + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relaySet +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onStart + +open class GenericRelayListCache( + val signer: NostrSigner, +) { + val cachedPrivateLists = PrivateTagArrayEventCache(signer) + + fun cachedRelays(event: T) = cachedPrivateLists.mergeTagListPrecached(event).relaySet() + + suspend fun relays(event: T) = cachedPrivateLists.mergeTagList(event).relaySet() + + fun fastStartValueForRelayList(note: Note): RelayListCard { + val noteEvent = note.event as? T + return if (noteEvent != null) { + RelayListCard(cachedRelays(noteEvent).toList()) + } else { + EmptyRelayListCard + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun observeDecryptedRelayList(note: Note): Flow = + note + .flow() + .metadata.stateFlow + .mapLatest { noteState -> + val event = noteState.note.event as? T + RelayListCard(event?.let { relays(it).toList() } ?: emptyList()) + }.onStart { + val event = note.event as? T + if (event != null) { + val list = relays(event) + if (list.isNotEmpty()) { + emit(RelayListCard(list.toList())) + } else { + emit(EmptyRelayListCard) + } + } else { + emit(EmptyRelayListCard) + } + }.distinctUntilChanged() + .flowOn(Dispatchers.Default) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt new file mode 100644 index 0000000000..bf12514c4c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayLists/RelayListCard.kt @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.relayLists + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +@Immutable +data class RelayListCard( + val relays: List, +) + +val EmptyRelayListCard = RelayListCard(emptyList()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt new file mode 100644 index 0000000000..bf88a37526 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListDecryptionCache.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays + +import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent + +class SearchRelayListDecryptionCache( + signer: NostrSigner, +) : GenericRelayListCache(signer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt similarity index 84% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt index 5fc3202159..3c5f2a25f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip50Search/SearchRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip50Search +package com.vitorpamplona.amethyst.model.nip51Lists.searchRelays import android.util.Log import com.vitorpamplona.amethyst.model.AccountSettings @@ -45,10 +45,11 @@ import kotlinx.coroutines.launch class SearchRelayListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: SearchRelayListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { - fun getSearchRelayListAddress() = SearchRelayListEvent.createAddress(signer.pubKey) + fun getSearchRelayListAddress() = SearchRelayListEvent.Companion.createAddress(signer.pubKey) fun getSearchRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getSearchRelayListAddress()) @@ -56,9 +57,9 @@ class SearchRelayListState( fun getSearchRelayList(): SearchRelayListEvent? = getSearchRelayListNote().event as? SearchRelayListEvent - fun normalizeSearchRelayListWithBackup(note: Note): Set { + suspend fun normalizeSearchRelayListWithBackup(note: Note): Set { val event = note.event as? SearchRelayListEvent ?: settings.backupSearchRelayList - return event?.relays()?.toSet() ?: DefaultSearchRelayList + return event?.let { decryptionCache.relays(it) } ?: DefaultSearchRelayList } val flow = @@ -68,29 +69,23 @@ class SearchRelayListState( .flowOn(Dispatchers.Default) .stateIn( scope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, emptySet(), ) - fun saveRelayList( - searchRelays: List, - onDone: (SearchRelayListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun saveRelayList(searchRelays: List): SearchRelayListEvent { val relayListForSearch = getSearchRelayList() - if (relayListForSearch != null && relayListForSearch.tags.isNotEmpty()) { - SearchRelayListEvent.updateRelayList( + return if (relayListForSearch != null && relayListForSearch.tags.isNotEmpty()) { + SearchRelayListEvent.Companion.updateRelayList( earlierVersion = relayListForSearch, relays = searchRelays, signer = signer, - onReady = onDone, ) } else { - SearchRelayListEvent.createFromScratch( + SearchRelayListEvent.Companion.create( relays = searchRelays, signer = signer, - onReady = onDone, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt new file mode 100644 index 0000000000..276b47abb9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListDecryptionCache.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays + +import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent + +class TrustedRelayListDecryptionCache( + signer: NostrSigner, +) : GenericRelayListCache(signer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt similarity index 78% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt index f1137b7bbe..d5bd692354 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/TrustedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip51Lists +package com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays import android.util.Log import com.vitorpamplona.amethyst.model.AccountSettings @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -44,20 +44,21 @@ import kotlinx.coroutines.launch class TrustedRelayListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: TrustedRelayListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { - fun getTrustedRelayListAddress() = TrustedRelayListEvent.createAddress(signer.pubKey) + fun getTrustedRelayListAddress() = TrustedRelayListEvent.Companion.createAddress(signer.pubKey) - fun getTrustedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getTrustedRelayListAddress()) + fun getTrustedRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getTrustedRelayListAddress()) fun getTrustedRelayListFlow(): StateFlow = getTrustedRelayListNote().flow().metadata.stateFlow fun getTrustedRelayList(): TrustedRelayListEvent? = getTrustedRelayListNote().event as? TrustedRelayListEvent - fun normalizeTrustedRelayListWithBackup(note: Note): Set { + suspend fun normalizeTrustedRelayListWithBackup(note: Note): Set { val event = note.event as? TrustedRelayListEvent ?: settings.backupTrustedRelayList - return event?.relays()?.toSet() ?: emptySet() + return event?.let { decryptionCache.relays(it) } ?: emptySet() } val flow = @@ -67,29 +68,23 @@ class TrustedRelayListState( .flowOn(Dispatchers.Default) .stateIn( scope, - SharingStarted.Eagerly, + SharingStarted.Companion.Eagerly, emptySet(), ) - fun saveRelayList( - trustedRelays: List, - onDone: (TrustedRelayListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun saveRelayList(trustedRelays: List): TrustedRelayListEvent { val relayListForTrusted = getTrustedRelayList() - if (relayListForTrusted != null && relayListForTrusted.tags.isNotEmpty()) { - TrustedRelayListEvent.updateRelayList( + return if (relayListForTrusted != null && relayListForTrusted.tags.isNotEmpty()) { + TrustedRelayListEvent.Companion.updateRelayList( earlierVersion = relayListForTrusted, relays = trustedRelays, signer = signer, - onReady = onDone, ) } else { - TrustedRelayListEvent.createFromScratch( + TrustedRelayListEvent.Companion.create( relays = trustedRelays, signer = signer, - onReady = onDone, ) } } @@ -98,7 +93,7 @@ class TrustedRelayListState( settings.backupTrustedRelayList?.let { Log.d("AccountRegisterObservers", "Loading saved Trusted relay list ${it.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) } } scope.launch(Dispatchers.Default) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt index 09bcc225f7..cb534fa395 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt @@ -28,23 +28,20 @@ import com.vitorpamplona.quartz.nip56Reports.ReportType class ReportAction { companion object { - fun report( + suspend fun report( user: User, type: ReportType, by: User, signer: NostrSigner, - onDone: (ReportEvent) -> Unit, - ) { - if (!signer.isWriteable()) return - + ): ReportEvent? { if (user.hasReport(by, type)) { // has already reported this note - return + return null } val template = ReportEvent.build(user.pubkeyHex, type) - signer.sign(template, onDone) + return signer.sign(template) } suspend fun report( @@ -53,17 +50,14 @@ class ReportAction { content: String = "", by: User, signer: NostrSigner, - onDone: (ReportEvent) -> Unit, - ) { - if (!signer.isWriteable()) return - + ): ReportEvent? { if (note.hasReport(by, type)) { // has already reported this note - return + return null } - note.event?.let { - signer.sign(ReportEvent.build(it, type), onDone) + return note.event?.let { + signer.sign(ReportEvent.build(it, type)) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt index 1018b03aec..9df03f4419 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt @@ -105,26 +105,19 @@ class Nip65RelayListState( emptySet(), ) - fun saveRelayList( - relays: List, - onDone: (AdvertisedRelayListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return - + suspend fun saveRelayList(relays: List): AdvertisedRelayListEvent { val nip65RelayList = getNIP65RelayList() - if (nip65RelayList != null) { + return if (nip65RelayList != null) { AdvertisedRelayListEvent.replaceRelayListWith( earlierVersion = nip65RelayList, newRelays = relays, signer = signer, - onReady = onDone, ) } else { AdvertisedRelayListEvent.createFromScratch( relays = relays, signer = signer, - onReady = onDone, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt new file mode 100644 index 0000000000..2ea70cb3a3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListDecryptionCache.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip72Communities + +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache +import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent +import com.vitorpamplona.quartz.nip72ModCommunities.follow.communities +import com.vitorpamplona.quartz.nip72ModCommunities.follow.communityIdSet +import com.vitorpamplona.quartz.nip72ModCommunities.follow.communityIds + +class CommunityListDecryptionCache( + val signer: NostrSigner, +) { + val cachedPrivateLists = PrivateTagArrayEventCache(signer) + + fun cachedCommunityIds(event: CommunityListEvent) = cachedPrivateLists.mergeTagListPrecached(event).communityIds() + + fun cachedCommunityIdSet(event: CommunityListEvent) = cachedPrivateLists.mergeTagListPrecached(event).communityIdSet() + + fun cachedCommunities(event: CommunityListEvent) = cachedPrivateLists.mergeTagListPrecached(event).communities() + + suspend fun communityIds(event: CommunityListEvent) = cachedPrivateLists.mergeTagList(event).communityIds() + + suspend fun communityIdSet(event: CommunityListEvent) = cachedPrivateLists.mergeTagList(event).communityIdSet() + + suspend fun communities(event: CommunityListEvent) = cachedPrivateLists.mergeTagList(event).communities() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt index 2690fb53be..9406632f6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt @@ -26,11 +26,10 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent -import com.vitorpamplona.quartz.utils.tryAndWait +import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -44,11 +43,11 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import kotlin.coroutines.resume class CommunityListState( val signer: NostrSigner, val cache: LocalCache, + val decryptionCache: CommunityListDecryptionCache, val scope: CoroutineScope, val settings: AccountSettings, ) { @@ -60,20 +59,13 @@ class CommunityListState( fun getCommunityList(): CommunityListEvent? = getCommunityListNote().event as? CommunityListEvent - suspend fun communityListWithBackup(note: Note): Set = - communityList( - note.event as? CommunityListEvent ?: settings.backupCommunityList, - ) - - suspend fun communityList(event: CommunityListEvent?): Set = - tryAndWait { continuation -> - event?.publicAndPrivateCommunities(signer) { - continuation.resume(it) - } - } ?: emptySet() + suspend fun communityListWithBackup(note: Note): Set { + val event = note.event as? CommunityListEvent ?: settings.backupCommunityList + return event?.let { decryptionCache.communities(it).toSet() } ?: emptySet() + } @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow> = + val flow: StateFlow> = getCommunityListFlow() .transformLatest { noteState -> emit(communityListWithBackup(noteState.note)) @@ -90,9 +82,9 @@ class CommunityListState( val flowSet: StateFlow> = flow .map { hint -> - hint.mapTo(mutableSetOf()) { it.addressId } + hint.mapTo(mutableSetOf()) { it.address.toValue() } }.onStart { - emit(flow.value.mapTo(mutableSetOf()) { it.addressId }) + emit(flow.value.mapTo(mutableSetOf()) { it.address.toValue() }) }.flowOn(Dispatchers.Default) .stateIn( scope, @@ -100,54 +92,48 @@ class CommunityListState( emptySet(), ) - fun follow( - communities: List, - onDone: (CommunityListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun follow(communities: List): CommunityListEvent { val communityList = getCommunityList() - val partialHint = communities.mapNotNull { it.toEventHint() } - if (communityList == null) { - CommunityListEvent.createCommunities(partialHint, true, signer, onReady = onDone) + val communityTags = + communities.mapNotNull { community -> + if (community.address.kind == CommunityDefinitionEvent.KIND) { + CommunityTag(community.address, community.relayHintUrl()) + } else { + null + } + } + + return if (communityList == null) { + CommunityListEvent.create(communityTags, true, signer) } else { - CommunityListEvent.addCommunities(communityList, partialHint, true, signer, onReady = onDone) + CommunityListEvent.add(communityList, communityTags, true, signer) } } - fun follow( - community: AddressableNote, - onDone: (CommunityListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun follow(community: AddressableNote): CommunityListEvent? { val communityList = getCommunityList() + if (community.address.kind != CommunityDefinitionEvent.KIND) return communityList - val fullHint = community.toEventHint() - if (fullHint != null) { - if (communityList == null) { - CommunityListEvent.createCommunity(fullHint, true, signer, onReady = onDone) - } else { - CommunityListEvent.addCommunity(communityList, fullHint, true, signer, onReady = onDone) - } + return if (communityList == null) { + CommunityListEvent.create( + CommunityTag(community.address, community.relayHintUrl()), + true, + signer, + ) } else { - val partialHint = community.toATag() - if (communityList == null) { - CommunityListEvent.createCommunity(partialHint, true, signer, onReady = onDone) - } else { - CommunityListEvent.addCommunity(communityList, partialHint, true, signer, onReady = onDone) - } + CommunityListEvent.add(communityList, CommunityTag(community.address, community.relayHintUrl()), true, signer) } } - fun unfollow( - community: AddressableNote, - onDone: (CommunityListEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun unfollow(community: AddressableNote): CommunityListEvent? { val communityList = getCommunityList() + if (community.address.kind != CommunityDefinitionEvent.KIND) return communityList - if (communityList != null) { - CommunityListEvent.removeCommunity(communityList, community.address.toValue(), signer, onReady = onDone) + return if (communityList != null) { + CommunityListEvent.remove(communityList, CommunityTag(community.address, community.relayHintUrl()), signer) + } else { + null } } @@ -156,9 +142,7 @@ class CommunityListState( Log.d("AccountRegisterObservers", "Loading saved Community list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - event.privateTags(signer) { - LocalCache.justConsumeMyOwnEvent(event) - } + LocalCache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt index e76ee05226..8a65a3de7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt @@ -53,17 +53,14 @@ class AppSpecificState( fun getAppSpecificDataFlow(): StateFlow = getAppSpecificDataNote().flow().metadata.stateFlow - fun saveNewAppSpecificData(onDone: (AppSpecificDataEvent) -> Unit) { + suspend fun saveNewAppSpecificData(): AppSpecificDataEvent { val toInternal = settings.syncedSettings.toInternal() - signer.nip44Encrypt(JsonMapper.mapper.writeValueAsString(toInternal), signer.pubKey) { encrypted -> - AppSpecificDataEvent.create( - dTag = APP_SPECIFIC_DATA_D_TAG, - description = encrypted, - otherTags = emptyArray(), - signer = signer, - onReady = onDone, - ) - } + return AppSpecificDataEvent.create( + dTag = APP_SPECIFIC_DATA_D_TAG, + description = signer.nip44Encrypt(JsonMapper.mapper.writeValueAsString(toInternal), signer.pubKey), + otherTags = emptyArray(), + signer = signer, + ) } init { @@ -72,16 +69,13 @@ class AppSpecificState( @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(event) - signer.decrypt(event.content, event.pubKey) { decrypted -> - try { - val syncedSettings = JsonMapper.mapper.readValue(decrypted) - settings.syncedSettings.updateFrom(syncedSettings) - } catch (e: Throwable) { - if (e is CancellationException) throw e - Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) - e.printStackTrace() - AccountSyncedSettingsInternal() - } + try { + val decrypted = signer.decrypt(event.content, event.pubKey) + val syncedSettings = JsonMapper.mapper.readValue(decrypted) + settings.syncedSettings.updateFrom(syncedSettings) + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value", e) } } } @@ -91,18 +85,13 @@ class AppSpecificState( getAppSpecificDataFlow().collect { Log.d("AccountRegisterObservers", "Updating AppSpecificData for ${signer.pubKey}") (it.note.event as? AppSpecificDataEvent)?.let { - signer.decrypt(it.content, it.pubKey) { decrypted -> - val syncedSettings = - try { - JsonMapper.mapper.readValue(decrypted) - } catch (e: Throwable) { - if (e is CancellationException) throw e - Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) - e.printStackTrace() - AccountSyncedSettingsInternal() - } - + val decrypted = signer.decrypt(it.content, it.pubKey) + try { + val syncedSettings = JsonMapper.mapper.readValue(decrypted) settings.updateAppSpecificData(it, syncedSettings) + } catch (e: Throwable) { + if (e is CancellationException) throw e + Log.w("LocalPreferences", "Error Decoding latestAppSpecificData from Preferences with value $decrypted", e) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt index 943dad95f9..05af740395 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip96FileStorage/FileStorageServerListState.kt @@ -66,11 +66,7 @@ class FileStorageServerListState( emptyList(), ) - fun saveFileServersList( - servers: List, - onDone: (FileServersEvent) -> Unit, - ) { - if (!signer.isWriteable()) return + suspend fun saveFileServersList(servers: List): FileServersEvent { val serverList = getFileServersList() val template = @@ -80,6 +76,6 @@ class FileStorageServerListState( FileServersEvent.build(servers) } - signer.sign(template, onDone) + return signer.sign(template) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt index 927a730c78..5d8557d35e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent -import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted @@ -38,7 +37,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlin.coroutines.resume class BlossomServerListState( val signer: NostrSigner, @@ -70,26 +68,19 @@ class BlossomServerListState( emptyList(), ) - fun saveBlossomServersList( - servers: List, - onDone: (BlossomServersEvent) -> Unit, - ) { - if (!signer.isWriteable()) return - + suspend fun saveBlossomServersList(servers: List): BlossomServersEvent { val serverList = getBlossomServersList() - if (serverList != null && serverList.tags.isNotEmpty()) { + return if (serverList != null && serverList.tags.isNotEmpty()) { BlossomServersEvent.updateRelayList( earlierVersion = serverList, relays = servers, signer = signer, - onReady = onDone, ) } else { BlossomServersEvent.createFromScratch( relays = servers, signer = signer, - onReady = onDone, ) } } @@ -98,26 +89,10 @@ class BlossomServerListState( hash: HexKey, size: Long, alt: String, - ): BlossomAuthorizationEvent? { - if (!signer.isWriteable()) return null - - return tryAndWait { continuation -> - BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, signer) { - continuation.resume(it) - } - } - } + ): BlossomAuthorizationEvent = BlossomAuthorizationEvent.createUploadAuth(hash, size, alt, signer) suspend fun createBlossomDeleteAuth( hash: HexKey, alt: String, - ): BlossomAuthorizationEvent? { - if (!signer.isWriteable()) return null - - return tryAndWait { continuation -> - BlossomAuthorizationEvent.createDeleteAuth(hash, alt, signer) { - continuation.resume(it) - } - } - } + ): BlossomAuthorizationEvent? = BlossomAuthorizationEvent.createDeleteAuth(hash, alt, signer) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt similarity index 79% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index e0e03a1b33..07e0b77bfe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -18,22 +18,27 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model +package com.vitorpamplona.amethyst.model.privateChats import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.flow.MutableStateFlow @Stable class Chatroom { - var authors: Set = setOf() + var activeSenders: Set = setOf() var roomMessages: Set = setOf() - var subject: String? = null + var subject = MutableStateFlow(null) var subjectCreatedAt: Long? = null + var ownerSentMessage: Boolean = false + var lastMessage: Note? = null @Synchronized fun addMessageSync(msg: Note) { @@ -43,15 +48,20 @@ class Chatroom { roomMessages = roomMessages + msg msg.author?.let { author -> - if (author !in authors) { - authors += author + if (author !in activeSenders) { + activeSenders += author } } + val createdAt = msg.createdAt() ?: 0 + if (createdAt > (lastMessage?.createdAt() ?: 0)) { + lastMessage = msg + } + val newSubject = msg.event?.subject() if (newSubject != null && (msg.createdAt() ?: 0) > (subjectCreatedAt ?: 0)) { - subject = newSubject + subject.tryEmit(newSubject) subjectCreatedAt = msg.createdAt() } } @@ -69,13 +79,13 @@ class Chatroom { .sortedBy { it.createdAt() } .lastOrNull() ?.let { - subject = it.event?.subject() + subject.tryEmit(it.event?.subject()) subjectCreatedAt = it.createdAt() } } } - fun senderIntersects(keySet: Set): Boolean = authors.any { it.pubkeyHex in keySet } + fun senderIntersects(keySet: Set): Boolean = activeSenders.any { it.pubkeyHex in keySet } fun pruneMessagesToTheLatestOnly(): Set { val sorted = roomMessages.sortedWith(DefaultFeedOrder) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt new file mode 100644 index 0000000000..797070e577 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.privateChats + +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.utils.LargeCache +import kotlinx.collections.immutable.persistentSetOf + +class ChatroomList( + val ownerPubKey: HexKey, +) { + var chatrooms = LargeCache() + private set + + private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom = chatrooms.getOrCreate(key) { Chatroom() } + + fun getOrCreatePrivateChatroom(user: User): Chatroom { + val key = ChatroomKey(persistentSetOf(user.pubkeyHex)) + return getOrCreatePrivateChatroom(key) + } + + fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom = getOrCreatePrivateChatroomSync(key) + + fun addMessage( + room: ChatroomKey, + msg: Note, + ) { + val privateChatroom = getOrCreatePrivateChatroom(room) + if (msg !in privateChatroom.roomMessages) { + privateChatroom.addMessageSync(msg) + } + } + + fun addMessage( + user: User, + msg: Note, + ) { + val privateChatroom = getOrCreatePrivateChatroom(user) + if (msg !in privateChatroom.roomMessages) { + privateChatroom.addMessageSync(msg) + if (msg.author?.pubkeyHex == ownerPubKey) { + privateChatroom.ownerSentMessage = true + } + } + } + + fun createChatroom(withKey: ChatroomKey) { + getOrCreatePrivateChatroom(withKey) + } + + fun removeMessage( + user: User, + msg: Note, + ) { + val privateChatroom = getOrCreatePrivateChatroom(user) + if (msg in privateChatroom.roomMessages) { + privateChatroom.removeMessageSync(msg) + } + } + + fun removeMessage( + room: ChatroomKey, + msg: Note, + ) { + val privateChatroom = getOrCreatePrivateChatroom(room) + if (msg in privateChatroom.roomMessages) { + privateChatroom.removeMessageSync(msg) + } + } + + fun hasSentMessagesTo(key: ChatroomKey?): Boolean { + if (key == null) return false + return chatrooms.get(key)?.ownerSentMessage == true + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt index ccee0916ef..9d0fed219a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState -import com.vitorpamplona.amethyst.model.nip51Lists.GeohashListState -import com.vitorpamplona.amethyst.model.nip51Lists.HashtagListState +import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState +import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState -import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted @@ -33,6 +33,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlin.collections.map class MergedFollowListsState( val kind3List: FollowListState, @@ -45,14 +46,14 @@ class MergedFollowListsState( kind3: FollowListState.Kind3Follows, hashtages: Set, geohashes: Set, - community: Set, + community: Set, ): FollowListState.Kind3Follows = FollowListState.Kind3Follows( kind3.authors, kind3.authorsPlusMe, kind3.hashtags + hashtages, kind3.geotags + geohashes, - kind3.communities + community.map { it.addressId }, + kind3.communities + community.map { it.address.toValue() }, ) val flow: StateFlow = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index ac719c9d1b..b016b1eaec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays -import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt index e0bf5faa0c..77e2797426 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState -import com.vitorpamplona.amethyst.model.nip50Search.SearchRelayListState -import com.vitorpamplona.amethyst.model.nip51Lists.TrustedRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt new file mode 100644 index 0000000000..2ab2797652 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedDecryptionCaches.kt @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds + +import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.PeopleListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache +import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache + +class FeedDecryptionCaches( + val peopleListCache: PeopleListDecryptionCache, + val muteListCache: MuteListDecryptionCache, + val communityListCache: CommunityListDecryptionCache, + val hashtagCache: HashtagListDecryptionCache, + val geohashCache: GeohashListDecryptionCache, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index dbaf694a0c..50af84a2be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -51,6 +51,7 @@ class FeedTopNavFilterState( val locationFlow: StateFlow, val followsRelays: StateFlow>, val blockedRelays: StateFlow>, + val caches: FeedDecryptionCaches, val signer: NostrSigner, val scope: CoroutineScope, ) { @@ -62,7 +63,7 @@ class FeedTopNavFilterState( else -> { val note = LocalCache.checkGetOrCreateAddressableNote(listName) if (note != null) { - NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays, blockedRelays) + NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays, blockedRelays, caches) } else { UnknownFeedFlow(listName) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt index 36b9672e4e..e2dc5baa92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.model.topNavFeeds.FeedDecryptionCaches import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedFlowsType import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavFilter @@ -32,11 +33,11 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthors import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent -import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -49,30 +50,31 @@ class NoteFeedFlow( val signer: NostrSigner, val allFollowRelays: StateFlow>, val blockedRelays: StateFlow>, + val caches: FeedDecryptionCaches, ) : IFeedFlowsType { fun process(noteEvent: Event): IFeedTopNavFilter = when (noteEvent) { is PeopleListEvent -> { if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { - MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays) + MutedAuthorsByOutboxTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), blockedRelays) } else { - AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays) + AuthorsByOutboxTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), blockedRelays) } } is MuteListEvent -> { - MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users, blockedRelays) + MutedAuthorsByOutboxTopNavFilter(caches.muteListCache.cachedUserIdSet(noteEvent), blockedRelays) } is FollowListEvent -> { - AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet(), blockedRelays) + AuthorsByOutboxTopNavFilter(noteEvent.followIdSet(), blockedRelays) } is CommunityListEvent -> { - AllCommunitiesTopNavFilter(noteEvent.publicAndCachedPrivateCommunityIds().toSet(), blockedRelays) + AllCommunitiesTopNavFilter(caches.communityListCache.cachedCommunityIdSet(noteEvent), blockedRelays) } is HashtagListEvent -> { - HashtagTopNavFilter(noteEvent.publicAndCachedPrivateHashtags(), allFollowRelays) + HashtagTopNavFilter(caches.hashtagCache.cachedHashtags(noteEvent), allFollowRelays) } is GeohashListEvent -> { - LocationTopNavFilter(noteEvent.publicAndCachedPrivateGeohash(), allFollowRelays) + LocationTopNavFilter(caches.geohashCache.cachedGeohashes(noteEvent), allFollowRelays) } is CommunityDefinitionEvent -> { SingleCommunityTopNavFilter( @@ -89,50 +91,25 @@ class NoteFeedFlow( when (noteEvent) { is PeopleListEvent -> { if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { - emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays)) - - noteEvent.publicAndPrivateUsersAndWords(signer)?.let { - emit(MutedAuthorsByOutboxTopNavFilter(it.users, blockedRelays)) - } + emit(MutedAuthorsByOutboxTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), blockedRelays)) } else { - emit(AuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedPrivateUsersAndWords().users, blockedRelays)) - - noteEvent.publicAndPrivateUsersAndWords(signer)?.let { - emit(AuthorsByOutboxTopNavFilter(it.users, blockedRelays)) - } + emit(AuthorsByOutboxTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), blockedRelays)) } } is MuteListEvent -> { - emit(MutedAuthorsByOutboxTopNavFilter(noteEvent.publicAndCachedUsersAndWords().users, blockedRelays)) - - noteEvent.publicAndPrivateUsersAndWords(signer)?.let { - emit(MutedAuthorsByOutboxTopNavFilter(it.users, blockedRelays)) - } + emit(MutedAuthorsByOutboxTopNavFilter(caches.muteListCache.mutedUserIdSet(noteEvent), blockedRelays)) } is FollowListEvent -> { - emit(AuthorsByOutboxTopNavFilter(noteEvent.pubKeys().toSet(), blockedRelays)) + emit(AuthorsByOutboxTopNavFilter(noteEvent.followIdSet(), blockedRelays)) } is CommunityListEvent -> { - emit(AllCommunitiesTopNavFilter(noteEvent.publicCommunityIds().toSet(), blockedRelays)) - - noteEvent.publicAndPrivateCommunities(signer)?.let { - val communities = it.map { it.addressId }.toSet() - emit(AllCommunitiesTopNavFilter(communities, blockedRelays)) - } + emit(AllCommunitiesTopNavFilter(caches.communityListCache.communityIdSet(noteEvent), blockedRelays)) } is HashtagListEvent -> { - emit(HashtagTopNavFilter(noteEvent.publicHashtags().toSet(), allFollowRelays)) - - noteEvent.publicAndPrivateHashtag(signer)?.let { - emit(HashtagTopNavFilter(it, allFollowRelays)) - } + emit(HashtagTopNavFilter(caches.hashtagCache.hashtags(noteEvent), allFollowRelays)) } is GeohashListEvent -> { - emit(LocationTopNavFilter(noteEvent.publicGeohashes().toSet(), allFollowRelays)) - - noteEvent.publicAndPrivateGeohash(signer)?.let { - emit(LocationTopNavFilter(it, allFollowRelays)) - } + emit(LocationTopNavFilter(caches.geohashCache.geohashes(noteEvent), allFollowRelays)) } is CommunityDefinitionEvent -> { emit( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt index c334861355..42adc194f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ZapPaymentHandler.kt @@ -38,16 +38,13 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import com.vitorpamplona.quartz.utils.collectSuccessfulOperationsReturning import com.vitorpamplona.quartz.utils.mapNotNullAsync -import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient -import kotlin.coroutines.resume import kotlin.math.round class ZapPaymentHandler( @@ -194,7 +191,7 @@ class ZapPaymentHandler( onProgress(0.75f) } - if (account.hasWalletConnectSetup()) { + if (account.nip47SignerState.hasWalletConnectSetup()) { payViaNWC(payables, note, onError = onError, onProgress = { onProgress(it * 0.25f + 0.75f) // keeps within range. }, context) @@ -234,7 +231,14 @@ class ZapPaymentHandler( // makes sure the zap split user receives the zap event val userRelayList = next.user?.inboxRelays()?.toSet() ?: emptySet() - val zapRequest = prepareZapRequestIfNeeded(note, pollOption, message, zapType, next.user, userRelayList + authorRelayList) + val noteEvent = note.event + + val zapRequest = + if (zapType != LnZapEvent.ZapType.NONZAP && noteEvent != null) { + account.createZapRequestFor(noteEvent, pollOption, message, zapType, next.user, userRelayList + authorRelayList) + } else { + null + } ZapRequestReady(next, zapRequest) } @@ -302,17 +306,12 @@ class ZapPaymentHandler( ): List { var progressAllPayments = 0.00f - return collectSuccessfulOperationsReturning( + return mapNotNullAsync( items = payables, - runRequestFor = { payable: Payable, onReady -> + runRequestFor = { payable: Payable -> account.sendZapPaymentRequestFor( bolt11 = payable.invoice, zappedNote = note, - onSent = { - progressAllPayments += 0.5f / payables.size - onProgress(progressAllPayments) - onReady(Paid(payable, true)) - }, onResponse = { response -> if (response is PayInvoiceErrorResponse) { progressAllPayments += 0.5f / payables.size @@ -333,6 +332,11 @@ class ZapPaymentHandler( } }, ) + + progressAllPayments += 0.5f / payables.size + onProgress(progressAllPayments) + + Paid(payable, true) }, ) } @@ -372,22 +376,4 @@ class ZapPaymentHandler( invoice = invoice, ) } - - private suspend fun prepareZapRequestIfNeeded( - note: Note, - pollOption: Int?, - message: String, - zapType: LnZapEvent.ZapType, - overrideUser: User? = null, - additionalRelays: Set? = null, - ): LnZapRequestEvent? = - if (zapType != LnZapEvent.ZapType.NONZAP) { - tryAndWait { continuation -> - account.createZapRequestFor(note, pollOption, message, zapType, overrideUser, additionalRelays) { zapRequest -> - continuation.resume(zapRequest) - } - } - } else { - null - } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt index 9650f2358f..22aba4a818 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v3/V3Parser.kt @@ -30,7 +30,7 @@ import java.util.Base64 import kotlin.coroutines.cancellation.CancellationException class V3Parser { - companion object Companion { + companion object { fun parseCashuA(cashuToken: String): GenericLoadable> { try { val base64token = cashuToken.replace("cashuA", "") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt index ddd22fb5e3..d26f54060c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Parser.kt @@ -65,7 +65,6 @@ class V4Parser { return GenericLoadable.Loaded(converted.toImmutableList()) } catch (e: Exception) { - e.printStackTrace() if (e is CancellationException) throw e return GenericLoadable.Error("Could not parse this cashu token") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt index c3bfc9bfd7..a9ae46535a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/LocationState.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.service.location import android.content.Context +import android.util.Log import com.fonfon.kgeohash.GeoHash import com.fonfon.kgeohash.toGeoHash import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeohashPrecision @@ -77,7 +78,7 @@ class LocationState( }.onEach { latestLocation = it }.catch { e -> - e.printStackTrace() + Log.w("GeohashStateFlow", "Exception in the flow", e) latestLocation = LocationResult.LackPermission emit(LocationResult.LackPermission) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/ReverseGeolocation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/ReverseGeolocation.kt index dc6ed4fd8e..27e0c3120a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/ReverseGeolocation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/location/ReverseGeolocation.kt @@ -84,6 +84,7 @@ class ReverseGeolocation { 1, ) } catch (e: IOException) { + Log.w("ReverseGeolocation", "IO Error", e) e.printStackTrace() return null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index cfd928f67c..75eeeff215 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -42,7 +42,6 @@ import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEven import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent @@ -86,12 +85,11 @@ class EventNotificationConsumer( ) { val signer = account.createSigner(applicationContext.contentResolver) - pushWrappedEvent.unwrapThrowing(signer) { notificationEvent -> - consumeNotificationEvent(notificationEvent, signer, account) - } + val notificationEvent = pushWrappedEvent.unwrapThrowing(signer) + consumeNotificationEvent(notificationEvent, signer, account) } - fun consumeNotificationEvent( + suspend fun consumeNotificationEvent( notificationEvent: Event, signer: NostrSigner, account: AccountSettings, @@ -100,22 +98,17 @@ class EventNotificationConsumer( Log.d(TAG, "New Notification ${notificationEvent.kind} ${notificationEvent.id} Arrived for ${signer.pubKey} consumed= $consumed") if (!consumed) { Log.d(TAG, "New Notification was verified") - unwrapAndConsume(notificationEvent, signer) { innerEvent -> - if (!notificationManager().areNotificationsEnabled()) return@unwrapAndConsume + if (!notificationManager().areNotificationsEnabled()) return + Log.d(TAG, "Notifications are enabled") - Log.d(TAG, "Unwrapped consume $consumed ${innerEvent.javaClass.simpleName}") - if (innerEvent is PrivateDmEvent) { - Log.d(TAG, "New Nip-04 DM to Notify") - notify(innerEvent, signer, account) - } else if (innerEvent is LnZapEvent) { - Log.d(TAG, "New Zap to Notify") - notify(innerEvent, signer, account) - } else if (innerEvent is ChatMessageEvent) { - Log.d(TAG, "New ChatMessage to Notify") - notify(innerEvent, signer, account) - } else if (innerEvent is ChatMessageEncryptedFileHeaderEvent) { - Log.d(TAG, "New ChatMessage File to Notify") - notify(innerEvent, signer, account) + unwrapAndConsume(notificationEvent, signer)?.let { innerEvent -> + Log.d(TAG, "Unwrapped consume ${innerEvent.javaClass.simpleName}") + + when (innerEvent) { + is PrivateDmEvent -> notify(innerEvent, signer, account) + is LnZapEvent -> notify(innerEvent, signer, account) + is ChatMessageEvent -> notify(innerEvent, signer, account) + is ChatMessageEncryptedFileHeaderEvent -> notify(innerEvent, signer, account) } } } @@ -146,42 +139,45 @@ class EventNotificationConsumer( } } - private fun unwrapAndConsume( + private suspend fun unwrapAndConsume( event: Event, signer: NostrSigner, - onReady: (Event) -> Unit, - ) { - if (LocalCache.hasConsumed(event)) return + ): Event? { + if (LocalCache.hasConsumed(event)) return null - when (event) { + return when (event) { is GiftWrapEvent -> { if (LocalCache.justConsume(event, null, false)) { // new event - event.unwrap(signer) { - // clear the encrypted payload to save memory - LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() + val inner = event.unwrapThrowing(signer) + // clear the encrypted payload to save memory + LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() - unwrapAndConsume(it, signer, onReady) - } + unwrapAndConsume(inner, signer) + } else { + null } } is SealedRumorEvent -> { if (LocalCache.justConsume(event, null, false)) { // new event - event.unseal(signer) { - // clear the encrypted payload to save memory - LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() + val inner = event.unsealThrowing(signer) + // clear the encrypted payload to save memory + LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() - // this is not verifiable - if (LocalCache.justConsume(it, null, true)) { - onReady(it) - } + // this is not verifiable + if (LocalCache.justConsume(inner, null, true)) { + event + } else { + null } + } else { + null } } else -> { LocalCache.justConsume(event, null, false) - onReady(event) + event } } } @@ -191,6 +187,7 @@ class EventNotificationConsumer( signer: NostrSigner, acc: AccountSettings, ) { + Log.d(TAG, "New ChatMessage File to Notify") if ( // old event being re-broadcasted event.createdAt > TimeUtils.fifteenMinutesAgo() && @@ -198,7 +195,7 @@ class EventNotificationConsumer( event.pubKey != signer.pubKey ) { // from the user Log.d(TAG, "Notifying") - val myUser = LocalCache.getUserIfExists(signer.pubKey) ?: return + val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) val chatNote = LocalCache.getNoteIfExists(event.id) ?: return val chatRoom = event.chatroomKey(signer.pubKey) @@ -206,8 +203,7 @@ class EventNotificationConsumer( val isKnownRoom = ( - myUser.privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true || - myUser.hasSentMessagesTo(chatRoom) + chatroomList.chatrooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) ) if (isKnownRoom) { @@ -236,6 +232,7 @@ class EventNotificationConsumer( signer: NostrSigner, acc: AccountSettings, ) { + Log.d(TAG, "New ChatMessage to Notify") if ( // old event being re-broadcasted event.createdAt > TimeUtils.fifteenMinutesAgo() && @@ -243,17 +240,13 @@ class EventNotificationConsumer( event.pubKey != signer.pubKey ) { // from the user Log.d(TAG, "Notifying") - val myUser = LocalCache.getUserIfExists(signer.pubKey) ?: return + val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) val chatNote = LocalCache.getNoteIfExists(event.id) ?: return val chatRoom = event.chatroomKey(signer.pubKey) val followingKeySet = acc.backupContactList?.unverifiedFollowKeySet()?.toSet() ?: return - val isKnownRoom = - ( - myUser.privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true || - myUser.hasSentMessagesTo(chatRoom) - ) + val isKnownRoom = chatroomList.chatrooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) if (isKnownRoom) { val content = chatNote.event?.content ?: "" @@ -274,13 +267,14 @@ class EventNotificationConsumer( } } - private fun notify( + private suspend fun notify( event: PrivateDmEvent, signer: NostrSigner, acc: AccountSettings, ) { + Log.d(TAG, "New Nip-04 DM to Notify") val note = LocalCache.getNoteIfExists(event.id) ?: return - val myUser = LocalCache.getUserIfExists(signer.pubKey) ?: return + val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) // old event being re-broadcast if (event.createdAt < TimeUtils.fifteenMinutesAgo()) return @@ -290,13 +284,11 @@ class EventNotificationConsumer( val chatRoom = event.chatroomKey(signer.pubKey) - val isKnownRoom = - myUser.privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true || - myUser.hasSentMessagesTo(chatRoom) + val isKnownRoom = chatroomList.chatrooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) if (isKnownRoom) { note.author?.let { - decryptContent(note, signer) { content -> + decryptContent(note, signer)?.let { content -> val user = note.author?.toBestDisplayName() ?: "" val userPicture = note.author?.profilePicture() val noteUri = note.toNEvent() + "?account=" + acc.keyPair.pubKey.toNpub() @@ -308,47 +300,44 @@ class EventNotificationConsumer( } } - fun decryptZapContentAuthor( - note: Note, + suspend fun decryptZapContentAuthor( + event: LnZapRequestEvent, signer: NostrSigner, - onReady: (Event) -> Unit, - ) { - val event = note.event - if (event is LnZapRequestEvent) { - if (event.isPrivateZap()) { - event.decryptPrivateZap(signer) { onReady(it) } - } else { - onReady(event) - } - } - } - - fun decryptContent( - note: Note, - signer: NostrSigner, - onReady: (String) -> Unit, - ) { - val event = note.event - if (event is PrivateDmEvent) { - event.plainContent(signer, onReady) - } else if (event is LnZapRequestEvent) { - decryptZapContentAuthor(note, signer) { onReady(it.content) } - } else if (event is DraftEvent) { - event.cachedDraft(signer) { - onReady(it.content) - } + ): Event? = + if (event.isPrivateZap() && event.zappedAuthor().contains(event.pubKey)) { + signer.decryptZapEvent(event) } else { - event?.content?.let { onReady(it) } + event + } + + suspend fun decryptContent( + note: Note, + signer: NostrSigner, + ): String? { + val event = note.event + when (event) { + is PrivateDmEvent -> { + return event.decryptContent(signer) + } + + is LnZapRequestEvent -> { + return decryptZapContentAuthor(event, signer)?.content + } + + else -> { + return event?.content + } } } - private fun notify( + private suspend fun notify( event: LnZapEvent, signer: NostrSigner, acc: AccountSettings, ) { + Log.d(TAG, "New Zap to Notify") Log.d(TAG, "Notify Start ${event.toNostrUri()}") - val noteZapEvent = LocalCache.getNoteIfExists(event.id) ?: return + LocalCache.getNoteIfExists(event.id) ?: return Log.d(TAG, "Notify Not Notified Yet") @@ -358,8 +347,7 @@ class EventNotificationConsumer( Log.d(TAG, "Notify Not an old event") val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) } ?: return - val noteZapped = - event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return + val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) } ?: return Log.d(TAG, "Notify ZapRequest $noteZapRequest zapped $noteZapped") @@ -373,17 +361,17 @@ class EventNotificationConsumer( Log.d(TAG, "Notify Amount $amount") (noteZapRequest.event as? LnZapRequestEvent)?.let { event -> - decryptZapContentAuthor(noteZapRequest, signer) { + decryptZapContentAuthor(event, signer)?.let { decryptedEvent -> Log.d(TAG, "Notify Decrypted if Private Zap ${event.id}") - val author = LocalCache.getOrCreateUser(it.pubKey) - val senderInfo = Pair(author, it.content.ifBlank { null }) + val author = LocalCache.getOrCreateUser(decryptedEvent.pubKey) + val senderInfo = Pair(author, decryptedEvent.content.ifBlank { null }) if (noteZapped.event?.content != null) { - decryptContent(noteZapped, signer) { + decryptContent(noteZapped, signer)?.let { decrypted -> Log.d(TAG, "Notify Decrypted if Private Note") - val zappedContent = it.split("\n").get(0) + val zappedContent = decrypted.split("\n")[0] val user = senderInfo.first.toBestDisplayName() var title = stringRes(applicationContext, R.string.app_notification_zaps_channel_message, amount) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt index b22599810a..43c2279b51 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/RegisterAccounts.kt @@ -30,7 +30,6 @@ import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.utils.mapNotNullAsync -import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType @@ -38,7 +37,6 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.coroutines.executeAsync -import kotlin.coroutines.resume class RegisterAccounts( private val accounts: List, @@ -61,12 +59,8 @@ class RegisterAccounts( } return mapNotNullAsync(remainingTos) { info -> - tryAndWait { continuation -> - val signer = info.accountSettings.createSigner(Amethyst.instance.contentResolver) - RelayAuthEvent.create(info.relays, notificationToken, signer) { result -> - continuation.resume(result) - } - } + val signer = info.accountSettings.createSigner(Amethyst.instance.contentResolver) + RelayAuthEvent.create(info.relays, notificationToken, signer) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt index 6954c39417..b880576921 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/authCommand/model/AuthCoordinator.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayAuthenticator +import kotlinx.coroutines.CoroutineScope class ScreenAuthAccount( val account: Account, @@ -32,11 +33,12 @@ class ScreenAuthAccount( class AuthCoordinator( client: NostrClient, + scope: CoroutineScope, ) { private val authWithAccounts = ListWithUniqueSetCache { it.account } val receiver = - RelayAuthenticator(client) { challenge, relay -> + RelayAuthenticator(client, scope) { challenge, relay -> authWithAccounts.distinct().forEach { it.sendAuthEvent(relay, challenge) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 13e0c59e5d..774bb3bded 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState.Companion.APP_SPECIFIC_DATA_D_TAG -import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -29,10 +28,11 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent +import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt index 7e65c20e4b..34dff46d7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -28,8 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt index fbb5c5b9b4..c0473e031b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterDraftsAndReportsFromKey.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip37Drafts.DraftEvent -import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent val ReportsAndBookmarksFromKeyKinds = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt index 8f17ce115a..d1e8a3ea45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt @@ -27,9 +27,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent val FollowAndMutesFromKeyKinds = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 23ef0ae476..2719793619 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -29,14 +29,15 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -277,9 +278,9 @@ fun observeUserTagFollowCount( .metadata.stateFlow .sample(1000) .mapLatest { noteState -> - (noteState.note.event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.size ?: 0 + (noteState.note.event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0 }.onStart { - emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.size ?: 0) + emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.size ?: 0) }.distinctUntilChanged() .flowOn(Dispatchers.Default) } @@ -305,9 +306,9 @@ fun observeUserTagFollows( .metadata.stateFlow .sample(200) .mapLatest { noteState -> - (noteState.note.event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.sorted() ?: emptyList() + (noteState.note.event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList() }.onStart { - emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.publicAndCachedPrivateHashtags()?.sorted() ?: emptyList()) + emit((accountViewModel.hashtagFollows(user).event as? HashtagListEvent)?.let { accountViewModel.account.hashtagListDecryptionCache.hashtags(it) }?.sorted() ?: emptyList()) }.distinctUntilChanged() .flowOn(Dispatchers.Default) } @@ -319,15 +320,21 @@ fun observeUserTagFollows( fun observeUserBookmarks( user: User, accountViewModel: AccountViewModel, -): State { +): State { // Subscribe in the relay for changes in the metadata of this user. UserFinderFilterAssemblerSubscription(user, accountViewModel) // Subscribe in the LocalCache for changes that arrive in the device - return user - .flow() - .bookmarks.stateFlow - .collectAsStateWithLifecycle() + val flow = + remember(user) { + accountViewModel + .bookmarks(user) + .flow() + .metadata.stateFlow + } + + // Subscribe in the LocalCache for changes that arrive in the device + return flow.collectAsStateWithLifecycle() } @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @@ -342,12 +349,13 @@ fun observeUserBookmarkCount( // Subscribe in the LocalCache for changes that arrive in the device val flow = remember(user) { - user + accountViewModel + .bookmarks(user) .flow() - .followers.stateFlow + .metadata.stateFlow .sample(200) - .mapLatest { userState -> - userState.user.latestBookmarkList?.countBookmarks() ?: 0 + .mapLatest { noteState -> + (noteState.note.event as? BookmarkListEvent)?.countBookmarks() ?: 0 }.distinctUntilChanged() .flowOn(Dispatchers.Default) } @@ -661,33 +669,6 @@ fun observeUserRelayIntoList( return flow.collectAsStateWithLifecycle(false) } -@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) -@Composable -fun observeUserRoomSubject( - user: User, - room: ChatroomKey, - accountViewModel: AccountViewModel, -): State { - // Subscribe in the relay for changes in the metadata of this user. - UserFinderFilterAssemblerSubscription(user, accountViewModel) - - // Subscribe in the LocalCache for changes that arrive in the device - val flow = - remember(user) { - user - .flow() - .messages - .stateFlow - .sample(1000) - .mapLatest { userState -> - userState.user.privateChatrooms[room]?.subject - }.distinctUntilChanged() - .flowOn(Dispatchers.Default) - } - - return flow.collectAsStateWithLifecycle(user.privateChatrooms[room]?.subject) -} - data class RelayUsage( val relays: List = emptyList(), val userRelayList: List = emptyList(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt index 8392cdf547..21b3ce2ff9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt @@ -36,10 +36,10 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent -import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/FrameStat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/FrameStat.kt new file mode 100644 index 0000000000..6be8af457c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/FrameStat.kt @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.speedLogger + +import android.util.Log +import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger.Companion.TAG +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.utils.LargeCache +import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.timer + +class FrameStat { + var eventCount = AtomicInteger(0) + var kinds = LargeCache() + + fun increment( + kind: Int, + subId: String, + relayUrl: NormalizedRelayUrl, + memory: Long, + ) { + eventCount.incrementAndGet() + + val kindGroup = kinds.get(kind) + if (kindGroup != null) { + kindGroup.increment(memory, subId, relayUrl) + } else { + val group = KindGroup() + group.increment(memory, subId, relayUrl) + kinds.put(kind, group) + } + } + + fun hasAnything() = eventCount.get() > 0 + + fun reset() { + eventCount.set(0) + kinds.forEach { key, value -> value.reset() } + } + + fun log() { + Log.d(TAG, "Events Per Second: ${eventCount.get()}") + kinds.forEach { key, value -> + if (value.count.get() > 0) { + Log.d(TAG, "-- Kind $key $value") + } + } + } + + init { + // Use a timer to reset the counter every second. + timer(name = "EventsPerSecondCounter", period = 1000, daemon = true) { + if (hasAnything()) { + log() + reset() + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/KindGroup.kt similarity index 52% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/KindGroup.kt index a4faa10c5c..4fa495f174 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelaySpeedLogger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/KindGroup.kt @@ -18,21 +18,14 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.service.relayClient +package com.vitorpamplona.amethyst.service.relayClient.speedLogger -import android.util.Log -import com.vitorpamplona.amethyst.service.relayClient.RelaySpeedLogger.Companion.TAG -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient -import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener -import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.utils.LargeCache import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong import kotlin.concurrent.atomics.ExperimentalAtomicApi -import kotlin.concurrent.timer @OptIn(ExperimentalAtomicApi::class) class KindGroup( @@ -81,92 +74,3 @@ class KindGroup( override fun toString() = "(${count.get()} - ${memory.get().div(MB)}kb); ${printSubs()}; ${printRelays()}" } - -class FrameStat { - var eventCount = AtomicInteger(0) - var kinds = LargeCache() - - fun increment( - kind: Int, - subId: String, - relayUrl: NormalizedRelayUrl, - memory: Long, - ) { - eventCount.incrementAndGet() - - val kindGroup = kinds.get(kind) - if (kindGroup != null) { - kindGroup.increment(memory, subId, relayUrl) - } else { - val group = KindGroup() - group.increment(memory, subId, relayUrl) - kinds.put(kind, group) - } - } - - fun hasAnything() = eventCount.get() > 0 - - fun reset() { - eventCount.set(0) - kinds.forEach { key, value -> value.reset() } - } - - fun log() { - Log.d(TAG, "Events Per Second: ${eventCount.get()}") - kinds.forEach { key, value -> - if (value.count.get() > 0) { - Log.d(TAG, "-- Kind $key $value") - } - } - } - - // Initialize the timer in the constructor. This ensures it starts when the - // ResettableCounter object is created. - init { - // Use a timer to reset the counter every second. - timer(name = "EventsPerSecondCounter", period = 1000, daemon = true) { - if (hasAnything()) { - log() - reset() - } - } - } -} - -/** - * Listens to NostrClient's onNotify messages from the relay - */ -class RelaySpeedLogger( - val client: NostrClient, -) { - companion object { - val TAG = RelaySpeedLogger::class.java.simpleName - } - - var current = FrameStat() - - private val clientListener = - object : IRelayClientListener { - /** A new message was received */ - override fun onEvent( - relay: IRelayClient, - subId: String, - event: Event, - arrivalTime: Long, - afterEOSE: Boolean, - ) { - current.increment(event.kind, subId, relay.url, event.countMemory()) - } - } - - init { - Log.d(TAG, "Init, Subscribe") - client.subscribe(clientListener) - } - - fun destroy() { - // makes sure to run - Log.d(TAG, "Destroy, Unsubscribe") - client.unsubscribe(clientListener) - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt new file mode 100644 index 0000000000..8cbc9a1cb3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.speedLogger + +import android.util.Log +import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger.Companion.TAG +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient + +/** + * Listens to NostrClient's onNotify messages from the relay + */ +class RelaySpeedLogger( + val client: NostrClient, +) { + companion object { + val TAG = RelaySpeedLogger::class.java.simpleName + } + + var current = FrameStat() + + private val clientListener = + object : IRelayClientListener { + /** A new message was received */ + override fun onEvent( + relay: IRelayClient, + subId: String, + event: Event, + arrivalTime: Long, + afterEOSE: Boolean, + ) { + current.increment(event.kind, subId, relay.url, event.countMemory()) + } + } + + init { + Log.d(TAG, "Init, Subscribe") + client.subscribe(clientListener) + } + + fun destroy() { + // makes sure to run + Log.d(TAG, "Destroy, Unsubscribe") + client.unsubscribe(clientListener) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt index 523545b7c5..424a3cd82f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/MultiOrchestrator.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch @@ -47,7 +47,6 @@ class MultiOrchestrator( fun first() = list.first() suspend fun upload( - scope: CoroutineScope, alt: String?, contentWarningReason: String?, mediaQuality: CompressorQuality, @@ -55,29 +54,30 @@ class MultiOrchestrator( account: Account, context: Context, ): Result { - val jobs = - list.map { item -> - scope.launch(Dispatchers.IO) { - item.orchestrator.upload( - item.media.uri, - item.media.mimeType, - alt, - contentWarningReason, - mediaQuality, - server, - account, - context, - ) + coroutineScope { + val jobs = + list.map { item -> + launch(Dispatchers.IO) { + item.orchestrator.upload( + item.media.uri, + item.media.mimeType, + alt, + contentWarningReason, + mediaQuality, + server, + account, + context, + ) + } } - } - jobs.joinAll() + jobs.joinAll() + } return computeFinalResults() } suspend fun uploadEncrypted( - scope: CoroutineScope, alt: String?, contentWarningReason: String?, mediaQuality: CompressorQuality, @@ -86,24 +86,25 @@ class MultiOrchestrator( account: Account, context: Context, ): Result { - val jobs = - list.map { item -> - scope.launch(Dispatchers.IO) { - item.orchestrator.uploadEncrypted( - item.media.uri, - item.media.mimeType, - alt, - contentWarningReason, - mediaQuality, - cipher, - server, - account, - context, - ) + coroutineScope { + val jobs = + list.map { item -> + launch(Dispatchers.IO) { + item.orchestrator.uploadEncrypted( + item.media.uri, + item.media.mimeType, + alt, + contentWarningReason, + mediaQuality, + cipher, + server, + account, + context, + ) + } } - } - - jobs.joinAll() + jobs.joinAll() + } return computeFinalResults() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index 232ba281a6..c6859a460c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip17Dm.files.encryption.NostrCipher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map @@ -166,6 +167,8 @@ class UploadOrchestrator { originalHash = originalHash, okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, ) + } catch (_: SignerExceptions.ReadOnlyException) { + error(R.string.login_with_a_private_key_to_be_able_to_upload) } catch (e: Exception) { if (e is CancellationException) throw e error(R.string.failed_to_upload_media, e.message ?: e.javaClass.simpleName) @@ -207,6 +210,8 @@ class UploadOrchestrator { originalHash = originalHash, originalContentType = contentTypeForResult, ) + } catch (_: SignerExceptions.ReadOnlyException) { + error(R.string.login_with_a_private_key_to_be_able_to_upload) } catch (e: Exception) { if (e is CancellationException) throw e error(R.string.failed_to_upload_media, e.message ?: e.javaClass.simpleName) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt index e473a9424d..55cf8147f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/nip96/ServerInfoRetriever.kt @@ -53,7 +53,7 @@ class ServerInfoRetriever { val body = response.body.string() parser.parse(baseUrl, body) } else { - throw RuntimeException( + throw Exception( "Resulting Message from $baseUrl is an error: ${response.code} ${response.message}", ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index ca69bdf2ad..269e4ed12e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -163,14 +163,14 @@ fun uriToRoute( is NEvent -> { routeFor( note = LocalCache.getOrCreateNote(nip19.hex), - loggedIn = account.userProfile(), + loggedIn = account, ) ?: Route.EventRedirect(nip19.hex) } is NAddress -> { routeFor( note = LocalCache.getOrCreateAddressableNote(nip19.address()), - loggedIn = account.userProfile(), + loggedIn = account, ) ?: Route.EventRedirect(nip19.aTag()) } @@ -179,12 +179,12 @@ fun uriToRoute( if (noteEvent is AddressableEvent) { routeFor( note = LocalCache.getOrCreateAddressableNote(noteEvent.address()), - loggedIn = account.userProfile(), + loggedIn = account, ) ?: Route.EventRedirect(noteEvent.addressTag()) } else { routeFor( note = LocalCache.getOrCreateNote(nip19.event.id), - loggedIn = account.userProfile(), + loggedIn = account, ) ?: Route.EventRedirect(nip19.event.id) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 5d10cc634a..59f877edee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -48,6 +48,8 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener.onError +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder import com.vitorpamplona.quartz.nip94FileMetadata.alt @@ -168,6 +170,23 @@ open class EditPostViewModel : ViewModel() { server: ServerName, onError: (String, String) -> Unit, context: Context, + ) = try { + uploadUnsafe(alt, sensitiveContent, mediaQuality, isPrivate, server, onError, context) + } catch (e: SignerExceptions.ReadOnlyException) { + onError( + stringRes(context, R.string.read_only_user), + stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events), + ) + } + + fun uploadUnsafe( + alt: String?, + sensitiveContent: Boolean, + mediaQuality: Int, + isPrivate: Boolean = false, + server: ServerName, + onError: (String, String) -> Unit, + context: Context, ) { viewModelScope.launch { val myAccount = account ?: return@launch @@ -177,7 +196,6 @@ open class EditPostViewModel : ViewModel() { val results = myMultiOrchestrator.upload( - viewModelScope, alt, if (sensitiveContent) "" else null, MediaCompressor.intToCompressorQuality(mediaQuality), @@ -189,21 +207,21 @@ open class EditPostViewModel : ViewModel() { if (results.allGood) { results.successful.forEach { state -> if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - account?.createNip95( - state.result.bytes, - headerInfo = state.result.fileHeader, - alt, - if (sensitiveContent) "" else null, - ) { nip95 -> - nip95attachments = nip95attachments + nip95 - val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } + val nip95 = + myAccount.createNip95( + byteArray = state.result.bytes, + headerInfo = state.result.fileHeader, + alt = alt, + contentWarningReason = if (sensitiveContent) "" else null, + ) + nip95attachments = nip95attachments + nip95 + val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } - note?.let { - message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) - } - - urlPreview = findUrlInMessage() + note?.let { + message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) } + + urlPreview = findUrlInMessage() } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = IMetaTagBuilder(state.result.url) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt index 5099ff3647..544cab2176 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt @@ -171,7 +171,7 @@ object MediaSaverToDisk { onSuccess() } catch (e: Exception) { if (e is CancellationException) throw e - e.printStackTrace() + Log.w("MediaSaverToDisk", "Unable to save", e) onError(e) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 4e6e8a1cbf..630f525738 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -39,13 +39,11 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withTimeoutOrNull -import kotlin.coroutines.resume @Stable open class NewMediaModel : ViewModel() { @@ -83,6 +81,19 @@ open class NewMediaModel : ViewModel() { context: Context, onSucess: () -> Unit, onError: (String, String) -> Unit, + ) = try { + uploadUnsafe(context, onSucess, onError) + } catch (e: SignerExceptions.ReadOnlyException) { + onError( + stringRes(context, R.string.read_only_user), + stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events), + ) + } + + fun uploadUnsafe( + context: Context, + onSucess: () -> Unit, + onError: (String, String) -> Unit, ) { viewModelScope.launch { val myAccount = account ?: return@launch @@ -94,7 +105,6 @@ open class NewMediaModel : ViewModel() { val results = myMultiOrchestrator.upload( - viewModelScope, caption, if (sensitiveContent) "" else null, MediaCompressor.intToCompressorQuality(mediaQualitySlider), @@ -135,14 +145,8 @@ open class NewMediaModel : ViewModel() { nip95s.map { // upload each file as an individual nip95 event. viewModelScope.launch(Dispatchers.IO) { - withTimeoutOrNull(30000) { - suspendCancellableCoroutine { continuation -> - account?.createNip95(it.bytes, headerInfo = it.fileHeader, caption, if (sensitiveContent) "" else null) { nip95 -> - account?.consumeAndSendNip95(nip95.first, nip95.second) - continuation.resume(true) - } - } - } + val nip95 = myAccount.createNip95(it.bytes, headerInfo = it.fileHeader, caption, if (sensitiveContent) "" else null) + myAccount.consumeAndSendNip95(nip95.first, nip95.second) } } @@ -150,20 +154,14 @@ open class NewMediaModel : ViewModel() { videosAndOthers.map { // upload each file as an individual nip95 event. viewModelScope.launch(Dispatchers.IO) { - withTimeoutOrNull(30000) { - suspendCancellableCoroutine { continuation -> - account?.sendHeader( - url = it.url, - magnetUri = it.magnet, - headerInfo = it.fileHeader, - alt = caption, - contentWarningReason = if (sensitiveContent) "" else null, - originalHash = it.uploadedHash, - ) { - continuation.resume(true) - } - } - } + account?.sendHeader( + url = it.url, + magnetUri = it.magnet, + headerInfo = it.fileHeader, + alt = caption, + contentWarningReason = if (sensitiveContent) "" else null, + originalHash = it.uploadedHash, + ) } } @@ -171,17 +169,11 @@ open class NewMediaModel : ViewModel() { if (imageUrls.isNotEmpty()) { listOf( viewModelScope.launch(Dispatchers.IO) { - withTimeoutOrNull(30000) { - suspendCancellableCoroutine { continuation -> - account?.sendAllAsOnePictureEvent( - urlHeaderInfo = imageUrls, - caption = caption, - contentWarningReason = if (sensitiveContent) "" else null, - ) { - continuation.resume(true) - } - } - } + account?.sendAllAsOnePictureEvent( + urlHeaderInfo = imageUrls, + caption = caption, + contentWarningReason = if (sensitiveContent) "" else null, + ) }, ) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 62f3ebfb65..c24c121cd4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity @@ -212,6 +213,9 @@ class NewUserMetadataViewModel : ViewModel() { onUploading(false) onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading)) } + } catch (_: SignerExceptions.ReadOnlyException) { + onUploading(false) + onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload)) } catch (e: Exception) { if (e is CancellationException) throw e onUploading(false) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index cdf010fb39..a3388f6ce0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -183,7 +183,7 @@ private fun DisplayNoteLink( val noteState by observeNote(it, accountViewModel) val noteIdDisplayNote = remember(noteState) { "@${noteState.note.idDisplayNote()}" } - val route = routeFor(it, accountViewModel.userProfile()) ?: Route.EventRedirect(hex) + val route = routeFor(it, accountViewModel.account) ?: Route.EventRedirect(hex) CreateClickableText( clickablePart = noteIdDisplayNote, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 6a0cc02f3c..46b0e71b94 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -767,7 +767,7 @@ private fun DisplayNoteFromTag( } else { ClickableTextPrimary( text = "@${baseNote.idNote().toShortDisplay()}", - onClick = { routeFor(baseNote, accountViewModel.userProfile())?.let { nav.nav(it) } }, + onClick = { routeFor(baseNote, accountViewModel.account)?.let { nav.nav(it) } }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt index 0448f0275b..8ab4de50c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt @@ -28,8 +28,8 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.utils.TimeUtils class FilterByListParams( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 729af27ec3..bfb98950e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -142,7 +142,7 @@ fun AppNavigation( composableFromEndArgs { RelayInformationScreen(it.url, accountViewModel, nav) } composableFromEndArgs { CommunityScreen(Address(it.kind, it.pubKeyHex, it.dTag), accountViewModel, nav) } - composableFromEndArgs { ChatroomScreen(it.id.toString(), it.message, it.replyId, it.draftId, accountViewModel, nav) } + composableFromEndArgs { ChatroomScreen(it.toKey(), it.message, it.replyId, it.draftId, accountViewModel, nav) } composableFromEndArgs { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) } composableFromEndArgs { PublicChatChannelScreen(it.id, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 81d2d0ee0c..dc1260c58c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.navigation.routes +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache @@ -50,7 +51,7 @@ import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent fun routeFor( note: Note, - loggedIn: User, + loggedIn: Account, ): Route? { val noteEvent = note.event ?: return Route.EventRedirect(note.idHex) @@ -59,10 +60,10 @@ fun routeFor( fun routeFor( noteEvent: Event, - loggedIn: User, + loggedIn: Account, ): Route? { if (noteEvent is DraftEvent) { - val innerEvent = noteEvent.preCachedDraft(loggedIn.pubkeyHex) + val innerEvent = loggedIn.draftsDecryptionCache.preCachedDraft(noteEvent) if (innerEvent is IsInPublicChatChannel) { innerEvent.channelId()?.let { @@ -77,9 +78,9 @@ fun routeFor( return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag) } } else if (innerEvent is ChatroomKeyable) { - val room = innerEvent.chatroomKey(loggedIn.pubkeyHex) - loggedIn.createChatroom(room) - return Route.Room(room.hashCode()) + val room = innerEvent.chatroomKey(loggedIn.userProfile().pubkeyHex) + loggedIn.chatroomList.createChatroom(room) + return Route.Room(room) } else if (innerEvent is AddressableEvent) { return Route.Note(noteEvent.aTag().toTag()) } else { @@ -102,9 +103,9 @@ fun routeFor( return Route.LiveActivityChannel(it.kind, it.pubKeyHex, it.dTag) } } else if (noteEvent is ChatroomKeyable) { - val room = noteEvent.chatroomKey(loggedIn.pubkeyHex) - loggedIn.createChatroom(room) - return Route.Room(room.hashCode()) + val room = noteEvent.chatroomKey(loggedIn.userProfile().pubkeyHex) + loggedIn.chatroomList.createChatroom(room) + return Route.Room(room) } else if (noteEvent is CommunityDefinitionEvent) { return Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag()) } else if (noteEvent is GiftWrapEvent) { @@ -159,18 +160,18 @@ fun routeToMessage( replyId: HexKey? = null, draftId: HexKey? = null, accountViewModel: AccountViewModel, -): Route = routeToMessage(room, draftMessage, replyId, draftId, accountViewModel.userProfile()) +): Route = routeToMessage(room, draftMessage, replyId, draftId, accountViewModel.account) fun routeToMessage( room: ChatroomKey, - draftMessage: String?, + draftMessage: String? = null, replyId: HexKey? = null, draftId: HexKey? = null, - fromUser: User, + account: Account, ): Route { - fromUser.createChatroom(room) + account.chatroomList.createChatroom(room) - return Route.Room(room.hashCode(), draftMessage, replyId, draftId) + return Route.Room(room, draftMessage, replyId, draftId) } fun routeToMessage( @@ -195,26 +196,26 @@ fun authorRouteFor(note: Note): Route.Profile? = note.author?.pubkeyHex?.let { R fun routeReplyTo( note: Note, - asUser: User, + account: Account, ): Route? { val noteEvent = note.event return when (noteEvent) { is TextNoteEvent -> Route.NewPost(baseReplyTo = note.idHex) is PrivateDmEvent -> routeToMessage( - room = noteEvent.chatroomKey(asUser.pubkeyHex), + room = noteEvent.chatroomKey(account.userProfile().pubkeyHex), draftMessage = null, replyId = noteEvent.id, draftId = null, - fromUser = asUser, + account = account, ) is ChatroomKeyable -> routeToMessage( - room = noteEvent.chatroomKey(asUser.pubkeyHex), + room = noteEvent.chatroomKey(account.userProfile().pubkeyHex), draftMessage = null, replyId = noteEvent.id, draftId = null, - fromUser = asUser, + account = account, ) is CommentEvent -> { if (noteEvent.isGeohashedScoped()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index bd9d01613e..c62974fb8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -24,6 +24,7 @@ import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavHostController import androidx.navigation.toRoute import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import kotlinx.serialization.Serializable sealed class Route { @@ -122,11 +123,20 @@ sealed class Route { ) : Route() @Serializable data class Room( - val id: Int, + val id: String, val message: String? = null, val replyId: HexKey? = null, val draftId: HexKey? = null, - ) : Route() + ) : Route() { + constructor(key: ChatroomKey, message: String? = null, replyId: HexKey? = null, draftId: HexKey? = null) : this( + id = key.users.joinToString(","), + message = message, + replyId = replyId, + draftId = draftId, + ) + + fun toKey(): ChatroomKey = ChatroomKey(id.split(",").toSet()) + } @Serializable data class RoomByAuthor( val id: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt index 961448f849..c031d040a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/FeedFilterSpinner.kt @@ -71,8 +71,8 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import kotlinx.collections.immutable.ImmutableList @OptIn(ExperimentalPermissionsApi::class) @@ -263,7 +263,7 @@ fun RenderOption( if (noteEvent is PeopleListEvent) { noteEvent.nameOrTitle() ?: option.note.dTag() } else if (noteEvent is FollowListEvent) { - noteEvent.nameOrTitle() ?: option.note.dTag() + noteEvent.title() ?: option.note.dTag() } else { option.note.dTag() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index b2f984ffaf..717a4439c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -84,7 +84,7 @@ fun BadgeCompose( onClick = { routeFor( note, - accountViewModel.userProfile(), + accountViewModel.account, )?.let { nav.nav(it) } }, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index 7ad06f15b8..70abe8becb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -85,7 +85,7 @@ fun MessageSetCompose( scope.launch { routeFor( baseNote, - accountViewModel.userProfile(), + accountViewModel.account, )?.let { nav.nav(it) } } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 04b114d9e5..6caa3dff45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -134,7 +134,7 @@ fun MultiSetCompose( .background(backgroundColor.value) .combinedClickable( onClick = { - scope.launch { routeFor(baseNote, accountViewModel.userProfile())?.let { nav.nav(it) } } + scope.launch { routeFor(baseNote, accountViewModel.account)?.let { nav.nav(it) } } }, onLongClick = { popupExpanded.value = true }, ).padding( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 3b91d38691..e55fbd8800 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -327,7 +327,7 @@ fun DisplayStatusInner( onClick = { routeFor( note, - accountViewModel.userProfile(), + accountViewModel.account, )?.let { nav.nav(it) } }, ) { @@ -349,7 +349,7 @@ fun DisplayStatusInner( onClick = { routeFor( it, - accountViewModel.userProfile(), + accountViewModel.account, )?.let { nav.nav(it) } }, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 61ffa5e0ca..73f178b671 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -183,12 +183,12 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent -import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent @@ -474,7 +474,7 @@ fun ClickableNote( } else { baseNote } - routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav.nav(it) } + routeFor(redirectToNote, accountViewModel.account)?.let { nav.nav(it) } }, onLongClick = showPopup, ).background(backgroundColor.value) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index dace09a023..84339a6af8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -94,7 +94,7 @@ import com.vitorpamplona.amethyst.ui.theme.isLight import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import kotlinx.coroutines.launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt index 35a4ae7da3..c448e01791 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNote.kt @@ -243,7 +243,8 @@ fun PollNote( ) { val pollViewModel: PollNoteViewModel = viewModel(key = "PollNoteViewModel${baseNote.idHex}") - pollViewModel.load(accountViewModel.account, baseNote) + pollViewModel.init(accountViewModel.account) + pollViewModel.load(baseNote) PollNote( baseNote = baseNote, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 525492b300..3692520687 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -50,7 +50,7 @@ data class PollOption( @Stable class PollNoteViewModel : ViewModel() { - private var account: Account? = null + private lateinit var account: Account private var pollNote: Note? = null private var pollEvent: PollNoteEvent? = null @@ -69,12 +69,12 @@ class PollNoteViewModel : ViewModel() { var canZap = mutableStateOf(false) var tallies: List = emptyList() - fun load( - acc: Account, - note: Note?, - ) { - if (acc != account || pollNote != note) { - account = acc + fun init(acc: Account) { + account = acc + } + + fun load(note: Note?) { + if (pollNote != note) { pollNote = note pollEvent = pollNote?.event as PollNoteEvent pollOptions = pollEvent?.pollOptions() @@ -221,7 +221,10 @@ class PollNoteViewModel : ViewModel() { ): Boolean = pollNote!!.zaps.any { val zapEvent = it.value?.event as? LnZapEvent - val privateZapAuthor = (it.key.event as? LnZapRequestEvent)?.cachedPrivateZap() + val privateZapAuthor = + (it.key.event as? LnZapRequestEvent)?.let { + account.privateZapsDecryptionCache.cachedPrivateZap(it) + } zapEvent?.zappedPollOption() == option && (it.key.author?.pubkeyHex == user.pubkeyHex || privateZapAuthor?.pubKey == user.pubkeyHex) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 56ad6b551f..303b2ea967 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -351,10 +351,9 @@ fun RenderZapRaiser( LaunchedEffect(key1 = zapsState) { zapsState?.note?.let { - accountViewModel.calculateZapraiser(baseNote) { newStatus -> - if (zapraiserStatus != newStatus) { - zapraiserStatus = newStatus - } + val newStatus = accountViewModel.calculateZapraiser(baseNote) + if (zapraiserStatus != newStatus) { + zapraiserStatus = newStatus } } } @@ -580,7 +579,7 @@ private fun ReplyReactionWithDialog( nav: INav, ) { ReplyReaction(baseNote, grayTint, accountViewModel) { - nav.nav { routeReplyTo(baseNote, accountViewModel.userProfile()) } + nav.nav { routeReplyTo(baseNote, accountViewModel.account) } } } @@ -1196,10 +1195,9 @@ fun ObserveZapAmountText( val zapAmountTxt by produceState(initialValue = showAmount(baseNote.zapsAmount), key1 = zapsState) { zapsState?.note?.let { - accountViewModel.calculateZapAmount(it) { newZapAmount -> - if (value != newZapAmount) { - value = newZapAmount - } + val newZapAmount = accountViewModel.calculateZapAmount(it) + if (value != newZapAmount) { + value = newZapAmount } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt index 38d0e45031..16060c726f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateReactionTypeDialog.kt @@ -52,6 +52,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -70,7 +71,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder @@ -97,17 +97,21 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEve import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class UpdateReactionTypeViewModel : ViewModel() { - var account: Account? = null + lateinit var accountViewModel: AccountViewModel + lateinit var account: Account var nextChoice by mutableStateOf(TextFieldValue("")) var reactionSet by mutableStateOf(listOf()) - fun load(myAccount: Account) { - this.account = myAccount - this.reactionSet = myAccount.settings.syncedSettings.reactions.reactionChoices.value + fun init(accountViewModel: AccountViewModel) { + this.accountViewModel = accountViewModel + this.account = accountViewModel.account + } + + fun load() { + this.reactionSet = account.settings.syncedSettings.reactions.reactionChoices.value } fun toListOfChoices(commaSeparatedAmounts: String): List = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 } @@ -134,8 +138,7 @@ class UpdateReactionTypeViewModel : ViewModel() { } fun sendPost() { - viewModelScope.launch(Dispatchers.IO) { - account?.changeReactionTypes(reactionSet) + accountViewModel.changeReactionTypes(reactionSet) { nextChoice = TextFieldValue("") } } @@ -144,14 +147,7 @@ class UpdateReactionTypeViewModel : ViewModel() { nextChoice = TextFieldValue("") } - fun hasChanged(): Boolean = - reactionSet != - account - ?.settings - ?.syncedSettings - ?.reactions - ?.reactionChoices - ?.value + fun hasChanged(): Boolean = reactionSet != account.settings.syncedSettings.reactions.reactionChoices.value } @Composable @@ -161,7 +157,11 @@ fun UpdateReactionTypeDialog( nav: INav, ) { val postViewModel: UpdateReactionTypeViewModel = viewModel() - postViewModel.load(accountViewModel.account) + postViewModel.init(accountViewModel) + + LaunchedEffect(postViewModel, accountViewModel) { + postViewModel.load() + } UpdateReactionTypeDialog(postViewModel, onClose, accountViewModel, nav) } @@ -414,7 +414,7 @@ private fun WatchAndRenderNote( Column( Modifier.fillMaxWidth().clickable { - scope.launch { routeFor(emojiPack, accountViewModel.userProfile())?.let { nav.nav(it) } } + scope.launch { routeFor(emojiPack, accountViewModel.account)?.let { nav.nav(it) } } }, ) { RenderEmojiPack( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 34aa68b64f..907f5bf9a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -60,7 +60,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -75,17 +74,13 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton import com.vitorpamplona.amethyst.ui.note.buttons.SaveButton import com.vitorpamplona.amethyst.ui.painterRes @@ -100,137 +95,10 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer -import com.vitorpamplona.quartz.nip19Bech32.decodePrivateKeyAsHexOrNull -import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey -import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -@Stable -class UpdateZapAmountViewModel : ViewModel() { - var account: Account? = null - - var nextAmount by mutableStateOf(TextFieldValue("")) - var amountSet by mutableStateOf(listOf()) - var walletConnectRelay by mutableStateOf(TextFieldValue("")) - var walletConnectPubkey by mutableStateOf(TextFieldValue("")) - var walletConnectSecret by mutableStateOf(TextFieldValue("")) - var selectedZapType by mutableStateOf(LnZapEvent.ZapType.PRIVATE) - - fun copyFromClipboard(text: String) { - if (text.isBlank()) { - return - } - updateNIP47(text) - } - - fun load(myAccount: Account) { - this.account = myAccount - this.amountSet = myAccount.settings.syncedSettings.zaps.zapAmountChoices.value - this.selectedZapType = myAccount.settings.syncedSettings.zaps.defaultZapType.value - - this.walletConnectPubkey = - myAccount.settings.zapPaymentRequest - ?.pubKeyHex - ?.let { TextFieldValue(it) } ?: TextFieldValue("") - this.walletConnectRelay = - myAccount.settings.zapPaymentRequest - ?.relayUri - ?.url - ?.let { TextFieldValue(it) } ?: TextFieldValue("") - this.walletConnectSecret = - myAccount.settings.zapPaymentRequest - ?.secret - ?.let { TextFieldValue(it) } ?: TextFieldValue("") - } - - fun toListOfAmounts(commaSeparatedAmounts: String): List = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 } - - fun addAmount() { - val newValue = nextAmount.text.trim().toLongOrNull() - if (newValue != null) { - amountSet = amountSet + newValue - } - - nextAmount = TextFieldValue("") - } - - fun removeAmount(amount: Long) { - amountSet = amountSet - amount - } - - fun sendPost() { - val nip47Update = - if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) { - val pubkeyHex = - try { - decodePublicKey(walletConnectPubkey.text.trim()).toHexKey() - } catch (e: Exception) { - if (e is CancellationException) throw e - null - } - - val relayUrl = walletConnectRelay.text.ifBlank { null }?.let { RelayUrlNormalizer.normalizeOrNull(it) } - val privKeyHex = walletConnectSecret.text.ifBlank { null }?.let { decodePrivateKeyAsHexOrNull(it) } - - if (pubkeyHex != null && relayUrl != null) { - Nip47WalletConnect.Nip47URINorm( - pubkeyHex, - relayUrl, - privKeyHex, - ) - } else { - null - } - } else { - null - } - - viewModelScope.launch(Dispatchers.IO) { - account?.updateZapAmounts(amountSet, selectedZapType, nip47Update) - - nextAmount = TextFieldValue("") - } - } - - fun cancel() { - nextAmount = TextFieldValue("") - } - - fun hasChanged(): Boolean = - ( - selectedZapType != - account - ?.settings - ?.syncedSettings - ?.zaps - ?.defaultZapType - ?.value || - amountSet != - account - ?.settings - ?.syncedSettings - ?.zaps - ?.zapAmountChoices - ?.value || - walletConnectPubkey.text != (account?.settings?.zapPaymentRequest?.pubKeyHex ?: "") || - walletConnectRelay.text != (account?.settings?.zapPaymentRequest?.relayUri ?: "") || - walletConnectSecret.text != (account?.settings?.zapPaymentRequest?.secret ?: "") - ) - - fun updateNIP47(uri: String) { - val contact = Nip47WalletConnect.parse(uri) - walletConnectPubkey = TextFieldValue(contact.pubKeyHex) - walletConnectRelay = TextFieldValue(contact.relayUri.url) - walletConnectSecret = TextFieldValue(contact.secret ?: "") - } -} - @Composable fun UpdateZapAmountDialog( onClose: () -> Unit, @@ -238,7 +106,11 @@ fun UpdateZapAmountDialog( accountViewModel: AccountViewModel, ) { val postViewModel: UpdateZapAmountViewModel = viewModel() - postViewModel.load(accountViewModel.account) + postViewModel.init(accountViewModel) + + LaunchedEffect(accountViewModel, postViewModel) { + postViewModel.load() + } UpdateZapAmountDialog(postViewModel, onClose, nip47uri, accountViewModel) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt new file mode 100644 index 0000000000..7195785f32 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountViewModel.kt @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip19Bech32.decodePrivateKeyAsHexOrNull +import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey +import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import kotlinx.coroutines.CancellationException + +@Stable +class UpdateZapAmountViewModel : ViewModel() { + lateinit var accountViewModel: AccountViewModel + + var nextAmount by mutableStateOf(TextFieldValue("")) + var amountSet by mutableStateOf(listOf()) + var walletConnectRelay by mutableStateOf(TextFieldValue("")) + var walletConnectPubkey by mutableStateOf(TextFieldValue("")) + var walletConnectSecret by mutableStateOf(TextFieldValue("")) + var selectedZapType by mutableStateOf(LnZapEvent.ZapType.PRIVATE) + + fun copyFromClipboard(text: String) { + if (text.isBlank()) { + return + } + updateNIP47(text) + } + + fun init(accountViewModel: AccountViewModel) { + this.accountViewModel = accountViewModel + } + + fun load() { + this.amountSet = accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices.value + this.selectedZapType = accountViewModel.account.settings.syncedSettings.zaps.defaultZapType.value + + val nip47 = accountViewModel.account.settings.zapPaymentRequest.value + + this.walletConnectPubkey = nip47?.pubKeyHex?.let { TextFieldValue(it) } ?: TextFieldValue("") + this.walletConnectRelay = nip47?.relayUri?.url?.let { TextFieldValue(it) } ?: TextFieldValue("") + this.walletConnectSecret = nip47?.secret?.let { TextFieldValue(it) } ?: TextFieldValue("") + } + + fun toListOfAmounts(commaSeparatedAmounts: String): List = commaSeparatedAmounts.split(",").map { it.trim().toLongOrNull() ?: 0 } + + fun addAmount() { + val newValue = nextAmount.text.trim().toLongOrNull() + if (newValue != null) { + amountSet = amountSet + newValue + } + + nextAmount = TextFieldValue("") + } + + fun removeAmount(amount: Long) { + amountSet = amountSet - amount + } + + fun sendPost() { + val nip47Update = + if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) { + val pubkeyHex = + try { + decodePublicKey(walletConnectPubkey.text.trim()).toHexKey() + } catch (e: Exception) { + if (e is CancellationException) throw e + null + } + + val relayUrl = walletConnectRelay.text.ifBlank { null }?.let { RelayUrlNormalizer.normalizeOrNull(it) } + val privKeyHex = walletConnectSecret.text.ifBlank { null }?.let { decodePrivateKeyAsHexOrNull(it) } + + if (pubkeyHex != null && relayUrl != null) { + Nip47WalletConnect.Nip47URINorm( + pubkeyHex, + relayUrl, + privKeyHex, + ) + } else { + null + } + } else { + null + } + + accountViewModel.updateZapAmounts(amountSet, selectedZapType, nip47Update) + + nextAmount = TextFieldValue("") + } + + fun cancel() { + nextAmount = TextFieldValue("") + } + + fun hasChanged(): Boolean = + ( + selectedZapType != accountViewModel.account.settings.syncedSettings.zaps.defaultZapType.value || + amountSet != accountViewModel.account.settings.syncedSettings.zaps.zapAmountChoices.value || + walletConnectPubkey.text != ( + accountViewModel.account.settings.zapPaymentRequest.value + ?.pubKeyHex ?: "" + ) || + walletConnectRelay.text != ( + accountViewModel.account.settings.zapPaymentRequest.value + ?.relayUri ?: "" + ) || + walletConnectSecret.text != ( + accountViewModel.account.settings.zapPaymentRequest.value + ?.secret ?: "" + ) + ) + + fun updateNIP47(uri: String) { + val contact = Nip47WalletConnect.parse(uri) + walletConnectPubkey = TextFieldValue(contact.pubKeyHex) + walletConnectRelay = TextFieldValue(contact.relayUri.url) + walletConnectSecret = TextFieldValue(contact.secret ?: "") + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index 4fe375a24e..a75771bb55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -60,7 +60,6 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @Composable fun MoreOptionsButton( @@ -187,7 +186,9 @@ fun NoteDropDownMenu( text = { Text(stringRes(R.string.copy_text)) }, onClick = { val lastNoteVersion = (editState?.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow?.value ?: note - accountViewModel.decrypt(lastNoteVersion) { clipboardManager.setText(AnnotatedString(it)) } + accountViewModel.decrypt(lastNoteVersion) { + clipboardManager.setText(AnnotatedString(it)) + } onDismiss() }, ) @@ -359,24 +360,20 @@ fun WatchBookmarksFollowsAndAccount( val showSensitiveContent by accountViewModel.showSensitiveContent().collectAsStateWithLifecycle() LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = showSensitiveContent) { - withContext(Dispatchers.IO) { - accountViewModel.isInPrivateBookmarks(note) { - val newState = - DropDownParams( - isFollowingAuthor = accountViewModel.isFollowing(note.author), - isPrivateBookmarkNote = it, - isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note), - isLoggedUser = accountViewModel.isLoggedUser(note.author), - isSensitive = note.event?.isSensitiveOrNSFW() ?: false, - showSensitiveContent = showSensitiveContent, - ) + val newState = + DropDownParams( + isFollowingAuthor = accountViewModel.isFollowing(note.author), + isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note), + isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note), + isLoggedUser = accountViewModel.isLoggedUser(note.author), + isSensitive = note.event?.isSensitiveOrNSFW() ?: false, + showSensitiveContent = showSensitiveContent, + ) - launch(Dispatchers.Main) { - onNew( - newState, - ) - } - } + launch(Dispatchers.Main) { + onNew( + newState, + ) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index aef75ef970..b8fa57650c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -85,7 +85,7 @@ fun ForkInformationRowLightColor( val noteState by observeNote(originalVersion, accountViewModel) val note = noteState?.note ?: return val author = note.author ?: return - val route = remember(note) { routeFor(note, accountViewModel.userProfile()) } + val route = remember(note) { routeFor(note, accountViewModel.account) } if (route != null) { Row(modifier) { @@ -130,7 +130,7 @@ fun ForkInformationRow( ) { val noteState by observeNote(originalVersion, accountViewModel) val note = noteState?.note ?: return - val route = remember(note) { routeFor(note, accountViewModel.userProfile()) } + val route = remember(note) { routeFor(note, accountViewModel.account) } if (route != null) { Row(modifier) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt index 5da9dc90e1..1826aa018c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ZapTheDevsCard.kt @@ -236,7 +236,7 @@ fun ZapTheDevsCard( if (noteEvent != null) { val route = remember(releaseNote) { - routeFor(releaseNote, accountViewModel.userProfile()) + routeFor(releaseNote, accountViewModel.account) } if (route != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 27218654cf..3e763cb79a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -65,6 +65,7 @@ import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.hasGeohashes import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags @@ -98,13 +99,11 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size -import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlin.coroutines.resume @Stable open class CommentPostViewModel : @@ -207,14 +206,12 @@ open class CommentPostViewModel : if (noteEvent is DraftEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { - accountViewModel.createTempDraftNote(noteEvent) { innerNote -> - if (innerNote != null) { - val oldTag = (draft.event as? AddressableEvent)?.dTag() - if (oldTag != null) { - draftTag.set(oldTag) - } - loadFromDraft(innerNote) + accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> + val oldTag = (draft.event as? AddressableEvent)?.dTag() + if (oldTag != null) { + draftTag.set(oldTag) } + loadFromDraft(innerNote) } } } @@ -319,11 +316,7 @@ open class CommentPostViewModel : } } - tryAndWait { continuation -> - accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) { - continuation.resume(it) - } - } + accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) accountViewModel?.deleteDraft(draftTag.current) @@ -372,7 +365,7 @@ open class CommentPostViewModel : if (replyingTo != null) { val eventHint = replyingTo.toEventHint() ?: return null - CommentEvent.Companion.replyBuilder( + CommentEvent.replyBuilder( msg = tagger.message, replyingTo = eventHint, ) { @@ -392,7 +385,7 @@ open class CommentPostViewModel : } } else { val externalIdentity = externalIdentity ?: return null - CommentEvent.Companion.replyExternalIdentity( + CommentEvent.replyExternalIdentity( msg = tagger.message, extId = externalIdentity, ) { @@ -420,7 +413,7 @@ open class CommentPostViewModel : myEmojiSet: List?, ): List { if (myEmojiSet == null) return emptyList() - return CustomEmoji.Companion.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> + return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag( it.code, @@ -437,6 +430,22 @@ open class CommentPostViewModel : server: ServerName, onError: (title: String, message: String) -> Unit, context: Context, + ) = try { + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context) + } catch (e: SignerExceptions.ReadOnlyException) { + onError( + stringRes(context, R.string.read_only_user), + stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events), + ) + } + + fun uploadUnsafe( + alt: String?, + contentWarningReason: String?, + mediaQuality: Int, + server: ServerName, + onError: (title: String, message: String) -> Unit, + context: Context, ) { viewModelScope.launch(Dispatchers.Default) { val myAccount = account ?: return@launch @@ -446,7 +455,6 @@ open class CommentPostViewModel : val results = myMultiOrchestrator.upload( - viewModelScope, alt, contentWarningReason, MediaCompressor.Companion.intToCompressorQuality(mediaQuality), @@ -458,14 +466,13 @@ open class CommentPostViewModel : if (results.allGood) { results.successful.forEach { state -> if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - account?.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) { nip95 -> - nip95attachments = nip95attachments + nip95 - val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } + val nip95 = myAccount.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) + nip95attachments = nip95attachments + nip95 + val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } - note?.let { - message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) - urlPreviews.update(message) - } + note?.let { + message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) + urlPreviews.update(message) } } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt index d373f98eb7..3c75950cd4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessage.kt @@ -62,7 +62,7 @@ fun RenderChatMessage( userRoom?.let { if (it.users.size > 1 || (it.users.size == 1 && note.author == accountViewModel.account.userProfile())) { ChatroomHeader(it, MaterialTheme.colorScheme.replyModifier.padding(10.dp), accountViewModel) { - routeFor(note, accountViewModel.userProfile())?.let { + routeFor(note, accountViewModel.account)?.let { nav.nav(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt index 4e65ca63d0..0b20944483 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/ChatMessageEncryptedFile.kt @@ -66,7 +66,7 @@ fun RenderChatMessageEncryptedFile( userRoom?.let { if (it.users.size > 1 || (it.users.size == 1 && note.author == accountViewModel.account.userProfile())) { ChatroomHeader(it, MaterialTheme.colorScheme.replyModifier.padding(10.dp), accountViewModel) { - routeFor(note, accountViewModel.userProfile())?.let { + routeFor(note, accountViewModel.account)?.let { nav.nav(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt index 996fa443b6..60346dd4a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/CommunityHeader.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture @@ -100,7 +101,7 @@ fun RenderCommunity( Row( MaterialTheme.colorScheme.innerPostModifier .clickable { - routeFor(baseNote, accountViewModel.userProfile())?.let { nav.nav(it) } + routeFor(baseNote, accountViewModel.account)?.let { nav.nav(it) } }.padding(Size10dp), ) { ShortCommunityHeader( @@ -373,11 +374,13 @@ fun JoinCommunityButton( note: AddressableNote, nav: INav, ) { - val scope = rememberCoroutineScope() - Button( modifier = Modifier.padding(horizontal = 3.dp), - onClick = { scope.launch(Dispatchers.IO) { accountViewModel.account.follow(note) } }, + onClick = { + scope.launch(Dispatchers.IO) { + accountViewModel.follow(note) + } + }, shape = ButtonBorder, colors = ButtonDefaults.buttonColors( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt index 0f0ebbd65b..ec18a8c735 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FollowList.kt @@ -61,7 +61,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FollowSetImageModifier import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -108,7 +108,7 @@ fun DisplayFollowList( } Text( - text = noteEvent.nameOrTitle() ?: noteEvent.dTag(), + text = noteEvent.title() ?: noteEvent.dTag(), fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt index 8a1936bbee..d25ae7476a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Highlight.kt @@ -271,7 +271,7 @@ fun DisplayEntryForNote( if (description != null) { ClickableTextPrimary( text = description, - onClick = { routeFor(note, accountViewModel.userProfile())?.let { nav.nav(it) } }, + onClick = { routeFor(note, accountViewModel.account)?.let { nav.nav(it) } }, ) } else { DisplayEvent(noteEvent.id, note.toNostrUri(), null, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt index 23503060d6..45d867a2d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PeopleList.kt @@ -55,7 +55,7 @@ import com.vitorpamplona.amethyst.ui.note.getGradient import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt index be3c353284..a17db97998 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PrivateMessage.kt @@ -77,7 +77,7 @@ fun RenderPrivateMessage( userRoom?.let { if (it.users.size > 1 || (it.users.size == 1 && note.author == accountViewModel.account.userProfile())) { ChatroomHeader(it, MaterialTheme.colorScheme.replyModifier.padding(10.dp), accountViewModel) { - routeFor(note, accountViewModel.userProfile())?.let { + routeFor(note, accountViewModel.account)?.let { nav.nav(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index efa085dddb..d8382e3055 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -43,8 +43,10 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.RelayListCard import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserRelayIntoList import com.vitorpamplona.amethyst.ui.components.ShowMoreButton import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -58,13 +60,8 @@ import com.vitorpamplona.quartz.nip01Core.core.firstTagValueFor import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent -import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent -import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @Composable @@ -79,7 +76,9 @@ fun DisplayRelaySet( val relays by remember(noteEvent) { mutableStateOf( - noteEvent.relays().toImmutableList(), + RelayListCard( + noteEvent.relays().toImmutableList(), + ), ) } @@ -110,14 +109,18 @@ fun DisplayNIP65RelayList( val writeRelays by remember(baseNote) { mutableStateOf( - noteEvent.writeRelaysNorm()?.toImmutableList() ?: persistentListOf(), + RelayListCard( + noteEvent.writeRelaysNorm() ?: emptyList(), + ), ) } val readRelays by remember(baseNote) { mutableStateOf( - noteEvent.readRelaysNorm()?.toImmutableList() ?: persistentListOf(), + RelayListCard( + noteEvent.readRelaysNorm() ?: emptyList(), + ), ) } @@ -152,7 +155,9 @@ fun DisplayDMRelayList( val relays by remember(baseNote) { mutableStateOf( - noteEvent.relays().toImmutableList(), + RelayListCard( + noteEvent.relays(), + ), ) } @@ -173,14 +178,9 @@ fun DisplaySearchRelayList( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = baseNote.event as? SearchRelayListEvent ?: return - - val relays by - remember(baseNote) { - mutableStateOf( - noteEvent.relays().toImmutableList(), - ) - } + val relays by accountViewModel.account.searchRelayListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle( + accountViewModel.account.searchRelayListDecryptionCache.fastStartValueForRelayList(baseNote), + ) DisplayRelaySet( relays, @@ -199,14 +199,9 @@ fun DisplayBlockedRelayList( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = baseNote.event as? BlockedRelayListEvent ?: return - - val relays by - remember(baseNote) { - mutableStateOf( - noteEvent.relays().toImmutableList(), - ) - } + val relays by accountViewModel.account.blockedRelayListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle( + accountViewModel.account.blockedRelayListDecryptionCache.fastStartValueForRelayList(baseNote), + ) DisplayRelaySet( relays, @@ -225,14 +220,9 @@ fun DisplayTrustedRelayList( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = baseNote.event as? TrustedRelayListEvent ?: return - - val relays by - remember(baseNote) { - mutableStateOf( - noteEvent.relays().toImmutableList(), - ) - } + val relays by accountViewModel.account.trustedRelayListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle( + accountViewModel.account.trustedRelayListDecryptionCache.fastStartValueForRelayList(baseNote), + ) DisplayRelaySet( relays, @@ -246,7 +236,7 @@ fun DisplayTrustedRelayList( @Composable fun DisplayRelaySet( - relays: ImmutableList, + relay: RelayListCard, relayListName: String, relayDescription: String?, backgroundColor: MutableState, @@ -257,9 +247,9 @@ fun DisplayRelaySet( val toMembersShow = if (expanded) { - relays + relay.relays } else { - relays.take(3) + relay.relays.take(3) } Text( @@ -313,7 +303,7 @@ fun DisplayRelaySet( } } - if (relays.size > 3 && !expanded) { + if (relay.relays.size > 3 && !expanded) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt index 7a75c42ffe..fa80c2a343 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt @@ -177,7 +177,7 @@ fun RenderTextModificationEvent( .clickable { routeFor( baseNote, - accountViewModel.userProfile(), + accountViewModel.account, )?.let { nav.nav(it) } }, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt index d7d1fff853..d9d91d3fb6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TorrentComment.kt @@ -228,7 +228,7 @@ fun ShortTorrentHeader( ShortTorrentHeader( title = noteEvent?.title() ?: TorrentEvent.ALT_DESCRIPTION, size = noteEvent?.totalSizeBytes()?.let { countToHumanReadableBytes(it) } ?: "--", - modifier.clickable { routeFor(baseNote, accountViewModel.userProfile())?.let { nav.nav(it) } }, + modifier.clickable { routeFor(baseNote, accountViewModel.account)?.let { nav.nav(it) } }, accountViewModel, nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 6cb102b28e..12b91d0680 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -301,7 +301,7 @@ class AccountStateViewModel : ViewModel() { backupNIP65RelayList = AdvertisedRelayListEvent.create(DefaultNIP65List, tempSigner), backupDMRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, tempSigner), backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), tempSigner), - backupChannelList = ChannelListEvent.create(DefaultChannels, tempSigner), + backupChannelList = ChannelListEvent.create(emptyList(), DefaultChannels, tempSigner), torSettings = TorSettingsFlow.build(torSettings), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt index b55718b840..a563316d3c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FollowListState.kt @@ -45,10 +45,10 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent @@ -297,7 +297,7 @@ class PeopleListName( return if (noteEvent is PeopleListEvent) { noteEvent.nameOrTitle() ?: note.dTag() } else if (noteEvent is FollowListEvent) { - noteEvent.nameOrTitle() ?: note.dTag() + noteEvent.title() ?: note.dTag() } else { note.dTag() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 024aab78d6..883571d996 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -54,7 +54,6 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.WarningType import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier @@ -94,10 +93,10 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable @@ -113,9 +112,10 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.Response -import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent -import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent @@ -128,7 +128,6 @@ import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.mapNotNullAsync -import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf @@ -151,7 +150,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient -import kotlin.coroutines.resume +import java.util.Locale @Stable class AccountViewModel( @@ -162,15 +161,15 @@ class AccountViewModel( Dao { val account = Account( - accountSettings, - accountSettings.createSigner(app.contentResolver), - app.locationManager.geohashStateFlow, - LocalCache, - app.client, - viewModelScope, + settings = accountSettings, + signer = accountSettings.createSigner(app.contentResolver), + geolocationFlow = app.locationManager.geohashStateFlow, + cache = LocalCache, + client = app.client, + scope = viewModelScope, ) - val newNotesPreProcessor = PrecacheNewNotesProcessor(account, LocalCache) + val newNotesPreProcessor = DecryptAndIndexProcessor(account, LocalCache) var firstRoute: Route? = null @@ -418,7 +417,7 @@ class AccountViewModel( fun hasBoosted(baseNote: Note): Boolean = account.hasBoosted(baseNote) fun deleteBoostsTo(note: Note) { - viewModelScope.launch(Dispatchers.IO) { account.delete(account.boostsTo(note)) } + runIOCatching { account.delete(account.boostsTo(note)) } } suspend fun calculateIfNoteWasZappedByAccount( @@ -430,42 +429,36 @@ class AccountViewModel( } } - suspend fun calculateZapAmount( - zappedNote: Note, - onZapAmount: (String) -> Unit, - ) { + suspend fun calculateZapAmount(zappedNote: Note): String = if (zappedNote.zapPayments.isNotEmpty()) { withContext(Dispatchers.Default) { - account.calculateZappedAmount(zappedNote) { onZapAmount(showAmount(it)) } + val it = account.calculateZappedAmount(zappedNote) + showAmount(it) } } else { - onZapAmount(showAmount(zappedNote.zapsAmount)) + showAmount(zappedNote.zapsAmount) } - } - suspend fun calculateZapraiser( - zappedNote: Note, - onZapraiserStatus: (ZapraiserStatus) -> Unit, - ) { + suspend fun calculateZapraiser(zappedNote: Note): ZapraiserStatus { val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0 - if (zappedNote.zapPayments.isNotEmpty()) { + return if (zappedNote.zapPayments.isNotEmpty()) { withContext(Dispatchers.Default) { - account.calculateZappedAmount(zappedNote) { newZapAmount -> - var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat() + val newZapAmount = account.calculateZappedAmount(zappedNote) + var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat() - if (percentage > 1) { - percentage = 1f + if (percentage > 1) { + percentage = 1f + } + + val newZapraiserProgress = percentage + val newZapraiserLeft = + if (percentage > 0.99) { + "0" + } else { + showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal()) } - val newZapraiserProgress = percentage - val newZapraiserLeft = - if (percentage > 0.99) { - "0" - } else { - showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal()) - } - onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft)) - } + ZapraiserStatus(newZapraiserProgress, newZapraiserLeft) } } else { var percentage = zappedNote.zapsAmount.div(zapraiserAmount.toBigDecimal()).toFloat() @@ -481,7 +474,8 @@ class AccountViewModel( } else { showAmount((zapraiserAmount * (1 - percentage)).toBigDecimal()) } - onZapraiserStatus(ZapraiserStatus(newZapraiserProgress, newZapraiserLeft)) + + ZapraiserStatus(newZapraiserProgress, newZapraiserLeft) } } @@ -532,7 +526,7 @@ class AccountViewModel( .map { val request = it.request.event as? LnZapRequestEvent if (request?.isPrivateZap() == true) { - val cachedPrivateRequest = request.cachedPrivateZap() + val cachedPrivateRequest = account.privateZapsDecryptionCache.cachedPrivateZap(request) if (cachedPrivateRequest != null) { ZapAmountCommentNotification( LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.request.author, @@ -566,7 +560,7 @@ class AccountViewModel( .map { val request = it.first.event as? LnZapRequestEvent if (request?.isPrivateZap() == true) { - val cachedPrivateRequest = request.cachedPrivateZap() + val cachedPrivateRequest = account.privateZapsDecryptionCache.cachedPrivateZap(request) if (cachedPrivateRequest != null) { ZapAmountCommentNotification( LocalCache.getUserIfExists(cachedPrivateRequest.pubKey) ?: it.first.author, @@ -681,25 +675,20 @@ class AccountViewModel( onProgress: (percent: Float) -> Unit, onPayViaIntent: (ImmutableList) -> Unit, zapType: LnZapEvent.ZapType? = null, - ) { - viewModelScope.launch(Dispatchers.IO) { - ZapPaymentHandler(account) - .zap( - note = note, - amountMilliSats = amountInMillisats, - pollOption = pollOption, - message = message, - context = context, - showErrorIfNoLnAddress = showErrorIfNoLnAddress, - okHttpClient = ::okHttpClientForMoney, - onError = onError, - onProgress = { - onProgress(it) - }, - onPayViaIntent = onPayViaIntent, - zapType = zapType ?: defaultZapType(), - ) - } + ) = runIOCatching { + ZapPaymentHandler(account).zap( + note = note, + amountMilliSats = amountInMillisats, + pollOption = pollOption, + message = message, + context = context, + showErrorIfNoLnAddress = showErrorIfNoLnAddress, + okHttpClient = ::okHttpClientForMoney, + onError = onError, + onProgress = { onProgress(it) }, + onPayViaIntent = onPayViaIntent, + zapType = zapType ?: defaultZapType(), + ) } fun report( @@ -707,29 +696,29 @@ class AccountViewModel( type: ReportType, content: String = "", ) { - viewModelScope.launch(Dispatchers.IO) { account.report(note, type, content) } + runIOCatching { account.report(note, type, content) } } fun report( user: User, type: ReportType, ) { - viewModelScope.launch(Dispatchers.IO) { + runIOCatching { account.report(user, type) account.hideUser(user.pubkeyHex) } } fun boost(note: Note) { - viewModelScope.launch(Dispatchers.IO) { account.boost(note) } + runIOCatching { account.boost(note) } } fun removeEmojiPack(emojiPack: Note) { - viewModelScope.launch(Dispatchers.IO) { account.removeEmojiPack(emojiPack) } + runIOCatching { account.removeEmojiPack(emojiPack) } } fun addEmojiPack(emojiPack: Note) { - viewModelScope.launch(Dispatchers.IO) { account.addEmojiPack(emojiPack) } + runIOCatching { account.addEmojiPack(emojiPack) } } fun addMediaToGallery( @@ -741,46 +730,43 @@ class AccountViewModel( hash: String?, mimeType: String?, ) { - viewModelScope.launch(Dispatchers.IO) { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) } + runIOCatching { account.addToGallery(hex, url, relay, blurhash, dim, hash, mimeType) } } fun removeFromMediaGallery(note: Note) { - viewModelScope.launch(Dispatchers.IO) { account.removeFromGallery(note) } + runIOCatching { account.removeFromGallery(note) } } fun hashtagFollows(user: User): Note = LocalCache.getOrCreateAddressableNote(HashtagListEvent.createAddress(user.pubkeyHex)) + fun bookmarks(user: User): Note = LocalCache.getOrCreateAddressableNote(BookmarkListEvent.createBookmarkAddress(user.pubkeyHex)) + fun addPrivateBookmark(note: Note) { - viewModelScope.launch(Dispatchers.IO) { account.addBookmark(note, true) } + runIOCatching { account.addBookmark(note, true) } } fun addPublicBookmark(note: Note) { - viewModelScope.launch(Dispatchers.IO) { account.addBookmark(note, false) } + runIOCatching { account.addBookmark(note, false) } } fun removePrivateBookmark(note: Note) { - viewModelScope.launch(Dispatchers.IO) { account.removeBookmark(note, true) } + runIOCatching { account.removeBookmark(note, true) } } fun removePublicBookmark(note: Note) { - viewModelScope.launch(Dispatchers.IO) { account.removeBookmark(note, false) } + runIOCatching { account.removeBookmark(note, false) } } - fun isInPrivateBookmarks( - note: Note, - onReady: (Boolean) -> Unit, - ) { - account.isInPrivateBookmarks(note, onReady) - } + fun isInPrivateBookmarks(note: Note): Boolean = account.isInPrivateBookmarks(note) fun isInPublicBookmarks(note: Note): Boolean = account.isInPublicBookmarks(note) fun broadcast(note: Note) { - viewModelScope.launch(Dispatchers.IO) { account.broadcast(note) } + runIOCatching { account.broadcast(note) } } fun timestamp(note: Note) { - viewModelScope.launch(Dispatchers.IO) { account.timestamp(note) } + runIOCatching { account.timestamp(note) } } var lastTimeItTriedToUpdateAttestations: Long = 0 @@ -790,22 +776,16 @@ class AccountViewModel( val now = TimeUtils.now() if (now - lastTimeItTriedToUpdateAttestations > TimeUtils.ONE_HOUR) { lastTimeItTriedToUpdateAttestations = now - viewModelScope.launch(Dispatchers.IO) { account.updateAttestations() } - } - } - - fun precomputeNewEvents(notes: Set) { - viewModelScope.launch(Dispatchers.Default) { - newNotesPreProcessor.run(notes) + runIOCatching { account.updateAttestations() } } } fun delete(notes: List) { - viewModelScope.launch(Dispatchers.IO) { account.delete(notes) } + runIOCatching { account.delete(notes) } } fun delete(note: Note) { - viewModelScope.launch(Dispatchers.IO) { account.delete(note) } + runIOCatching { account.delete(note) } } fun cachedDecrypt(note: Note): String? = account.cachedDecryptContent(note) @@ -815,56 +795,82 @@ class AccountViewModel( fun decrypt( note: Note, onReady: (String) -> Unit, - ) { - viewModelScope.launch(Dispatchers.IO) { account.decryptContent(note, onReady) } + ) = runIOCatching { + account.decryptContent(note)?.let { onReady(it) } + } + + inline fun runIOCatching(crossinline action: suspend () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + try { + action() + } catch (e: SignerExceptions.ReadOnlyException) { + toastManager.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_sign_events, + ) + } catch (e: SignerExceptions.UnauthorizedDecryptionException) { + toastManager.toast( + R.string.unauthorized_exception, + R.string.unauthorized_exception_description, + ) + } + } + } + + fun follow(community: AddressableNote) { + runIOCatching { account.follow(community) } } fun follow(channel: PublicChatChannel) { - viewModelScope.launch(Dispatchers.IO) { account.follow(channel) } + runIOCatching { account.follow(channel) } } fun follow(channel: EphemeralChatChannel) { - viewModelScope.launch(Dispatchers.IO) { account.follow(channel) } + runIOCatching { account.follow(channel) } + } + + fun unfollow(community: AddressableNote) { + runIOCatching { account.unfollow(community) } } fun unfollow(channel: PublicChatChannel) { - viewModelScope.launch(Dispatchers.IO) { account.unfollow(channel) } + runIOCatching { account.unfollow(channel) } } fun unfollow(channel: EphemeralChatChannel) { - viewModelScope.launch(Dispatchers.IO) { account.unfollow(channel) } + runIOCatching { account.unfollow(channel) } } fun follow(user: User) { - viewModelScope.launch(Dispatchers.IO) { account.follow(user) } + runIOCatching { account.follow(user) } } fun unfollow(user: User) { - viewModelScope.launch(Dispatchers.IO) { account.unfollow(user) } + runIOCatching { account.unfollow(user) } } fun followGeohash(tag: String) { - viewModelScope.launch(Dispatchers.IO) { account.followGeohash(tag) } + runIOCatching { account.followGeohash(tag) } } fun unfollowGeohash(tag: String) { - viewModelScope.launch(Dispatchers.IO) { account.unfollowGeohash(tag) } + runIOCatching { account.unfollowGeohash(tag) } } fun followHashtag(tag: String) { - viewModelScope.launch(Dispatchers.IO) { account.followHashtag(tag) } + runIOCatching { account.followHashtag(tag) } } fun unfollowHashtag(tag: String) { - viewModelScope.launch(Dispatchers.IO) { account.unfollowHashtag(tag) } + runIOCatching { account.unfollowHashtag(tag) } } fun showWord(word: String) { - viewModelScope.launch(Dispatchers.IO) { account.showWord(word) } + runIOCatching { account.showWord(word) } } fun hideWord(word: String) { - viewModelScope.launch(Dispatchers.IO) { account.hideWord(word) } + runIOCatching { account.hideWord(word) } } fun isLoggedUser(pubkeyHex: HexKey?): Boolean = account.signer.pubKey == pubkeyHex @@ -878,20 +884,12 @@ class AccountViewModel( fun isFollowing(user: HexKey): Boolean = account.isFollowing(user) - fun updateContentWarnings(value: WarningType) { - viewModelScope.launch(Dispatchers.IO) { - account.updateShowSensitiveContent(value.prefCode) - } - } - fun markDonatedInThisVersion() = account.markDonatedInThisVersion() fun dontTranslateFrom() = account.settings.syncedSettings.languages.dontTranslateFrom fun dontTranslateFromFilteredBySpokenLanguages() = account.settings.syncedSettings.dontTranslateFromFilteredBySpokenLanguages() - fun toggleDontTranslateFrom(languageCode: String) = account.toggleDontTranslateFrom(languageCode) - fun translateTo() = account.settings.syncedSettings.languages.translateTo fun defaultZapType() = account.settings.syncedSettings.zaps.defaultZapType.value @@ -908,65 +906,62 @@ class AccountViewModel( fun filterSpamFromStrangers() = account.settings.syncedSettings.security.filterSpamFromStrangers - fun updateWarnReports(warnReports: Boolean) { - viewModelScope.launch(Dispatchers.IO) { - account.updateWarnReports(warnReports) - } - } + fun updateWarnReports(warnReports: Boolean) = runIOCatching { account.updateWarnReports(warnReports) } - fun updateFilterSpam(filterSpam: Boolean) { - viewModelScope.launch(Dispatchers.IO) { + fun updateFilterSpam(filterSpam: Boolean) = + runIOCatching { if (account.updateFilterSpam(filterSpam)) { LocalCache.antiSpam.active = filterSpamFromStrangers().value } } + + fun updateShowSensitiveContent(show: Boolean?) = runIOCatching { account.updateShowSensitiveContent(show) } + + fun changeReactionTypes( + reactionSet: List, + onDone: () -> Unit, + ) = runIOCatching { + account.changeReactionTypes(reactionSet) + onDone() } - fun unwrap( - event: GiftWrapEvent, - onReady: (Event) -> Unit, - ) { - account.unwrap(event, onReady) - } + fun updateZapAmounts( + amountSet: List, + selectedZapType: LnZapEvent.ZapType, + nip47Update: Nip47WalletConnect.Nip47URINorm?, + ) = runIOCatching { account.updateZapAmounts(amountSet, selectedZapType, nip47Update) } - fun unseal( - event: SealedRumorEvent, - onReady: (Event) -> Unit, - ) { - account.unseal(event, onReady) - } + fun toggleDontTranslateFrom(languageCode: String) = runIOCatching { account.toggleDontTranslateFrom(languageCode) } - fun show(user: User) { - viewModelScope.launch(Dispatchers.IO) { account.showUser(user.pubkeyHex) } - } + fun updateTranslateTo(languageCode: Locale) = runIOCatching { account.updateTranslateTo(languageCode) } - fun hide(user: User) { - viewModelScope.launch(Dispatchers.IO) { account.hideUser(user.pubkeyHex) } - } + fun prefer( + source: String, + target: String, + preference: String, + ) = runIOCatching { account.prefer(source, target, preference) } - fun hide(word: String) { - viewModelScope.launch(Dispatchers.IO) { account.hideWord(word) } - } + fun show(user: User) = runIOCatching { account.showUser(user.pubkeyHex) } - fun showUser(pubkeyHex: String) { - viewModelScope.launch(Dispatchers.IO) { account.showUser(pubkeyHex) } - } + fun hide(user: User) = runIOCatching { account.hideUser(user.pubkeyHex) } - fun createStatus(newStatus: String) { - viewModelScope.launch(Dispatchers.IO) { account.createStatus(newStatus) } - } + fun hide(word: String) = runIOCatching { account.hideWord(word) } + + fun showUser(pubkeyHex: String) = runIOCatching { account.showUser(pubkeyHex) } + + fun createStatus(newStatus: String) = runIOCatching { account.createStatus(newStatus) } fun updateStatus( address: Address, newStatus: String, ) { - viewModelScope.launch(Dispatchers.IO) { + runIOCatching { account.updateStatus(LocalCache.getOrCreateAddressableNote(address), newStatus) } } fun deleteStatus(address: Address) { - viewModelScope.launch(Dispatchers.IO) { + runIOCatching { account.deleteStatus(LocalCache.getOrCreateAddressableNote(address)) } } @@ -1196,24 +1191,6 @@ class AccountViewModel( .sortedBy { account.isFollowing(it) } .reversed() - fun loadUsers( - event: GeneralListEvent, - onReady: (ImmutableList) -> Unit, - ) { - viewModelScope.launch(Dispatchers.Default) { - account.decryptPeopleList(event) { privateTagList -> - onReady( - (event.taggedUserIds() + event.filterUsers(privateTagList)) - .toSet() - .mapNotNull { hex -> checkGetOrCreateUser(hex) } - .sortedBy { account.isFollowing(it) } - .reversed() - .toImmutableList(), - ) - } - } - } - fun checkVideoIsOnline( videoUrl: String, onDone: (Boolean) -> Unit, @@ -1252,7 +1229,7 @@ class AccountViewModel( viewModelScope.launch(Dispatchers.IO) { for (note in notes) { note.event?.createdAt?.let { date -> - val route = routeFor(note, accountViewModel.account.userProfile()) + val route = routeFor(note, accountViewModel.account) route?.let { if (route is Route.Room) { account.markAsRead("Room/${route.id}", date) @@ -1269,12 +1246,12 @@ class AccountViewModel( fun createChatRoomFor( user: User, - then: (Int) -> Unit, + then: (ChatroomKey) -> Unit, ) { viewModelScope.launch(Dispatchers.IO) { val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex)) - account.userProfile().createChatroom(withKey) - then(withKey.hashCode()) + account.chatroomList.createChatroom(withKey) + then(withKey) } } @@ -1292,12 +1269,13 @@ class AccountViewModel( override fun create(modelClass: Class): T = AccountViewModel(accountSettings, settings, app) as T } - private var collectorJob: Job? = null + private var collectorJobNew: Job? = null + private var collectorJobDeleted: Job? = null private val bundlerInsert = BundledInsert>(3000, Dispatchers.Default) init { Log.d("Init", "AccountViewModel") - collectorJob = + collectorJobNew = viewModelScope.launch(Dispatchers.Default) { feedStates.init() // awaits for init to finish before starting to capture new events. @@ -1311,7 +1289,24 @@ class AccountViewModel( logTime("AccountViewModel newEventBundle Update with ${newNotes.size} new notes") { feedStates.updateFeedsWith(newNotes) upgradeAttestations() - precomputeNewEvents(newNotes) + viewModelScope.launch(Dispatchers.Default) { + newNotesPreProcessor.runNew(newNotes) + } + } + } + } + + collectorJobDeleted = + viewModelScope.launch(Dispatchers.Default) { + LocalCache.live.deletedEventBundles.collect { newNotes -> + if (isDebug) { + Log.d( + "Rendering Metrics", + "Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()} with ${newNotes.size} new notes", + ) + } + logTime("AccountViewModel deletedEventBundle Update with ${newNotes.size} new notes") { + newNotesPreProcessor.runDeleted(newNotes) } } } @@ -1321,7 +1316,8 @@ class AccountViewModel( Log.d("Init", "AccountViewModel onCleared") feedStates.destroy() bundlerInsert.cancel() - collectorJob?.cancel() + collectorJobNew?.cancel() + collectorJobDeleted?.cancel() super.onCleared() } @@ -1431,92 +1427,87 @@ class AccountViewModel( } } - fun unwrapIfNeeded( - event: Event?, - onReady: (Note) -> Unit, - ) { - when (event) { - is GiftWrapEvent -> { - event.innerEventId?.let { - val existingNote = LocalCache.getNoteIfExists(it) - if (existingNote != null) { - unwrapIfNeeded(existingNote.event, onReady) - } else { - event.unwrap(account.signer) { - // clear the encrypted payload to save memory - LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() + private suspend fun unwrapGiftWrap(event: GiftWrapEvent): Note? { + val cacheInnerEventId = event.innerEventId + return if (cacheInnerEventId != null) { + val existingNoteEvent = LocalCache.getNoteIfExists(cacheInnerEventId)?.event + if (existingNoteEvent != null) { + unwrapIfNeeded(existingNoteEvent) + } else { + val newEvent = event.unwrapOrNull(account.signer) - if (LocalCache.justConsume(it, null, false)) { - unwrapIfNeeded(it, onReady) - } - } - } - } ?: run { - event.unwrap(account.signer) { - // clear the encrypted payload to save memory - LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() + if (newEvent == null) return null - val existingNote = LocalCache.getNoteIfExists(it.id) - if (existingNote != null) { - unwrapIfNeeded(existingNote.event, onReady) - } else { - if (LocalCache.justConsume(it, null, false)) { - unwrapIfNeeded(it, onReady) - } - } - } - } + // clear the encrypted payload to save memory + LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() + + LocalCache.justConsume(newEvent, null, false) + + unwrapIfNeeded(newEvent) } - is SealedRumorEvent -> { - event.innerEventId?.let { - val existingNote = LocalCache.getNoteIfExists(it) - if (existingNote != null) { - unwrapIfNeeded(existingNote.event, onReady) - } else { - event.unseal(account.signer) { - // clear the encrypted payload to save memory - LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() + } else { + val newEvent = event.unwrapThrowing(account.signer) + // clear the encrypted payload to save memory + LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() - // this is not verifiable - if (LocalCache.justConsume(it, null, true)) { - unwrapIfNeeded(it, onReady) - } - } - } - } ?: run { - event.unseal(account.signer) { - // clear the encrypted payload to save memory - LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() + val existingNoteEvent = LocalCache.getNoteIfExists(newEvent.id)?.event - val existingNote = LocalCache.getNoteIfExists(it.id) - if (existingNote != null) { - unwrapIfNeeded(existingNote.event, onReady) - } else { - // this is not verifiable - if (LocalCache.justConsume(it, null, true)) { - unwrapIfNeeded(it, onReady) - } - } - } - } - } - else -> { - event?.id?.let { - LocalCache.getNoteIfExists(it)?.let { - onReady(it) - } - } + if (existingNoteEvent != null) { + unwrapIfNeeded(existingNoteEvent) + } else { + LocalCache.justConsume(newEvent, null, false) + unwrapIfNeeded(newEvent) } } } + private suspend fun unwrapSeal(event: SealedRumorEvent): Note? { + val cacheInnerEventId = event.innerEventId + return if (cacheInnerEventId != null) { + val existingNoteEvent = LocalCache.getNoteIfExists(cacheInnerEventId)?.event + if (existingNoteEvent != null) { + unwrapIfNeeded(existingNoteEvent) + } else { + val newEvent = event.unsealThrowing(account.signer) + // clear the encrypted payload to save memory + LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() + + // this is not verifiable + LocalCache.justConsume(newEvent, null, true) + unwrapIfNeeded(newEvent) + } + } else { + val newEvent = event.unsealThrowing(account.signer) + // clear the encrypted payload to save memory + LocalCache.getOrCreateNote(event.id).event = event.copyNoContent() + + val existingNoteEvent = LocalCache.getNoteIfExists(newEvent.id)?.event + if (existingNoteEvent != null) { + unwrapIfNeeded(existingNoteEvent) + } else { + // this is not verifiable + LocalCache.justConsume(newEvent, null, true) + unwrapIfNeeded(newEvent) + } + } + } + + private suspend fun unwrapIfNeeded(event: Event): Note? = + when (event) { + is GiftWrapEvent -> unwrapGiftWrap(event) + is SealedRumorEvent -> unwrapSeal(event) + else -> LocalCache.getNoteIfExists(event.id) + } + fun unwrapIfNeeded( note: Note?, - onReady: (Note) -> Unit, - ) { - viewModelScope.launch(Dispatchers.IO) { - unwrapIfNeeded(note?.event) { - onReady(it) + onReady: (Note) -> Unit = {}, + ) = runIOCatching { + val noteEvent = note?.event + if (noteEvent != null) { + val resultingNote = unwrapIfNeeded(noteEvent) + if (resultingNote != null && resultingNote != note) { + onReady(resultingNote) } } } @@ -1543,12 +1534,7 @@ class AccountViewModel( account.deleteDraft(draftTag) } - suspend fun createTempDraftNote( - noteEvent: DraftEvent, - onReady: (Note?) -> Unit, - ) { - draftNoteCache.update(noteEvent, onReady) - } + suspend fun createTempDraftNote(noteEvent: DraftEvent): Note? = draftNoteCache.update(noteEvent) fun createTempDraftNote( innerEvent: Event, @@ -1600,12 +1586,11 @@ class AccountViewModel( fun sendZapPaymentRequestFor( bolt11: String, zappedNote: Note?, - onSent: () -> Unit, + onSent: () -> Unit = {}, onResponse: (Response?) -> Unit, - ) { - viewModelScope.launch(Dispatchers.IO) { - account.sendZapPaymentRequestFor(bolt11, zappedNote, onSent, onResponse) - } + ) = runIOCatching { + account.sendZapPaymentRequestFor(bolt11, zappedNote, onResponse) + onSent() } fun getRelayListFor(user: User): AdvertisedRelayListEvent? = (getRelayListNoteFor(user)?.event as? AdvertisedRelayListEvent?) @@ -1649,7 +1634,12 @@ class AccountViewModel( ) { viewModelScope.launch(Dispatchers.IO) { try { - val zapRequest = prepareZapRequestIfNeeded(user, message, defaultZapType()) + val zapRequest = + if (defaultZapType() != LnZapEvent.ZapType.NONZAP) { + account.createZapRequestFor(user, message, defaultZapType()) + } else { + null + } val invoice = LightningAddressResolver().lnAddressInvoice( @@ -1672,21 +1662,6 @@ class AccountViewModel( } } - private suspend fun prepareZapRequestIfNeeded( - user: User, - message: String, - zapType: LnZapEvent.ZapType, - ): LnZapRequestEvent? = - if (zapType != LnZapEvent.ZapType.NONZAP) { - tryAndWait { continuation -> - account.createZapRequestFor(user, message, zapType) { zapRequest -> - continuation.resume(zapRequest) - } - } - } else { - null - } - fun saveMediaToGallery( videoUri: String?, mimeType: String?, @@ -1741,16 +1716,16 @@ class AccountViewModel( class CachedDraftNotes( val accountViewModel: AccountViewModel, ) : GenericBaseCacheAsync(20) { - override suspend fun compute( - key: DraftEvent, - onReady: (Note?) -> Unit, - ) = withContext(Dispatchers.IO) { - key.cachedDraft(accountViewModel.account.signer) { - val author = LocalCache.getOrCreateUser(key.pubKey) - val note = accountViewModel.createTempDraftNote(it, author) - onReady(note) + override suspend fun compute(key: DraftEvent): Note? = + withContext(Dispatchers.IO) { + val decrypted = accountViewModel.account.draftsDecryptionCache.cachedDraft(key) + if (decrypted != null) { + val author = LocalCache.getOrCreateUser(key.pubKey) + accountViewModel.createTempDraftNote(decrypted, author) + } else { + null + } } - } } val bechLinkCache = CachedLoadedBechLink(this) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt new file mode 100644 index 0000000000..2343b07b7c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -0,0 +1,382 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import android.util.Log +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import kotlinx.coroutines.CancellationException + +class DecryptAndIndexProcessor( + val account: Account, + val cache: LocalCache, +) { + suspend fun consume(note: Note) { + val noteEvent = note.event + if (noteEvent != null) { + try { + consumeAlreadyVerified(noteEvent, note, note) + } catch (e: Exception) { + Log.e("PrecacheNewNotesProcessor", "Error processing note", e) + } + } + } + + suspend fun consumeAlreadyVerified( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + when (event) { + is PrivateDmEvent -> { + if (event.canDecrypt(account.signer.pubKey)) { + val talkingWith = event.chatroomKey(account.signer.pubKey) + account.chatroomList.addMessage(talkingWith, eventNote) + } + } + + is ChatMessageEvent -> { + if (event.isIncluded(account.signer.pubKey)) { + val key = event.chatroomKey(account.signer.pubKey) + account.chatroomList.addMessage(key, eventNote) + } + } + + is ChatMessageEncryptedFileHeaderEvent -> { + if (event.isIncluded(account.signer.pubKey)) { + val key = event.chatroomKey(account.signer.pubKey) + account.chatroomList.addMessage(key, eventNote) + } + } + + is OtsEvent -> { + // verifies new OTS upon arrival + Amethyst.instance.otsVerifCache.cacheVerify(event, account::otsResolver) + } + + is DraftEvent -> { + // Avoid decrypting over and over again if the event already exist. + if (event.pubKey == account.signer.pubKey) { + if (!event.isDeleted()) { + if (account.draftsDecryptionCache.preCachedDraft(event) == null) { + account.draftsDecryptionCache.cachedDraft(event)?.let { rumor -> + indexDraftAsRealEvent(eventNote, rumor) + } + } + } + } + } + + is GiftWrapEvent -> { + if (event.recipientPubKey() == account.signer.pubKey) { + val innerGiftId = event.innerEventId + if (innerGiftId == null) { + event.unwrapOrNull(account.signer)?.let { innerGift -> + // clear the encrypted payload to save memory + eventNote.event = event.copyNoContent() + if (cache.justConsume(innerGift, null, false)) { + cache.copyRelaysFromTo(publicNote, innerGift) + val innerGiftNote = cache.getOrCreateNote(innerGift.id) + consumeAlreadyVerified(innerGift, innerGiftNote, publicNote) + } + } + } else { + cache.copyRelaysFromTo(publicNote, innerGiftId) + + val innerGiftNote = cache.getOrCreateNote(innerGiftId) + val innerGift = innerGiftNote.event + if (innerGift != null) { + consumeAlreadyVerified(innerGift, innerGiftNote, publicNote) + } + } + } + } + + is SealedRumorEvent -> { + val rumorId = event.innerEventId + if (rumorId == null) { + event.unsealOrNull(account.signer)?.let { innerRumor -> + // clear the encrypted payload to save memory + eventNote.event = event.copyNoContent() + + // rumors cannot be verified + cache.justConsume(innerRumor, null, true) + + cache.copyRelaysFromTo(publicNote, innerRumor) + val innerRumorNote = cache.getOrCreateNote(innerRumor.id) + consumeAlreadyVerified(innerRumor, innerRumorNote, publicNote) + } + } else { + cache.copyRelaysFromTo(publicNote, rumorId) + + val innerRumorNote = cache.getOrCreateNote(rumorId) + val innerRumor = innerRumorNote.event + if (innerRumor != null) { + consumeAlreadyVerified(innerRumor, innerRumorNote, publicNote) + } + } + } + + is LnZapRequestEvent -> { + if (account.privateZapsDecryptionCache.cachedPrivateZap(event) == null && event.isPrivateZap()) { + account.privateZapsDecryptionCache.decryptPrivateZap(event) + } + } + } + } + + suspend fun delete(note: Note) { + val noteEvent = note.event + if (noteEvent != null) { + try { + delete(noteEvent, note) + } catch (e: Exception) { + Log.e("PrecacheNewNotesProcessor", "Error processing note", e) + } + } + } + + suspend fun delete( + event: Event, + eventNote: Note, + ) { + when (event) { + is PrivateDmEvent -> { + if (event.canDecrypt(account.signer.pubKey)) { + // Avoid decrypting over and over again if the event already exist. + val talkingWith = event.chatroomKey(account.signer.pubKey) + account.chatroomList.removeMessage(talkingWith, eventNote) + } + } + + is ChatMessageEvent -> { + if (event.isIncluded(account.signer.pubKey)) { + val key = event.chatroomKey(account.signer.pubKey) + account.chatroomList.removeMessage(key, eventNote) + } + } + + is ChatMessageEncryptedFileHeaderEvent -> { + if (event.isIncluded(account.signer.pubKey)) { + val key = event.chatroomKey(account.signer.pubKey) + account.chatroomList.removeMessage(key, eventNote) + } + } + + is DraftEvent -> { + // Avoid decrypting over and over again if the event already exist. + if (event.pubKey == account.signer.pubKey) { + if (!event.isDeleted()) { + if (account.draftsDecryptionCache.preCachedDraft(event) == null) { + account.draftsDecryptionCache.cachedDraft(event)?.let { + deindexDraftAsRealEvent(eventNote, it) + } + account.draftsDecryptionCache.delete(event) + } + } + } + } + + is GiftWrapEvent -> { + if (event.recipientPubKey() == account.signer.pubKey) { + val innerGiftId = event.innerEventId + if (innerGiftId != null) { + val innerGiftNote = cache.getNoteIfExists(innerGiftId) + val innerGift = innerGiftNote?.event + if (innerGift != null) { + delete(innerGift, innerGiftNote) + } + } + } + } + + is SealedRumorEvent -> { + val rumorId = event.innerEventId + if (rumorId != null) { + val innerRumorNote = cache.getNoteIfExists(rumorId) + val innerRumor = innerRumorNote?.event + if (innerRumor != null) { + delete(innerRumor, innerRumorNote) + } + } + } + + is LnZapEvent -> { + event.zapRequest?.let { req -> + if (req.isPrivateZap()) { + // We can't know which account this was for without going through it. + account.privateZapsDecryptionCache.delete(req) + } + } + } + + is LnZapRequestEvent -> { + if (event.isPrivateZap()) { + account.privateZapsDecryptionCache.delete(event) + } + } + // .. + } + } + + fun indexDraftAsRealEvent( + draftEventWrap: Note, + rumor: Event, + ) { + when (rumor) { + is PrivateDmEvent -> { + if (rumor.canDecrypt(account.signer)) { + val talkingWith = rumor.chatroomKey(account.signer.pubKey) + account.chatroomList.addMessage(talkingWith, draftEventWrap) + } + } + is ChatMessageEvent -> { + if (rumor.isIncluded(account.signer.pubKey)) { + val key = rumor.chatroomKey(account.signer.pubKey) + account.chatroomList.addMessage(key, draftEventWrap) + } + } + is ChatMessageEncryptedFileHeaderEvent -> { + if (rumor.isIncluded(account.signer.pubKey)) { + val key = rumor.chatroomKey(account.signer.pubKey) + account.chatroomList.addMessage(key, draftEventWrap) + } + } + is EphemeralChatEvent -> { + rumor.roomId()?.let { + cache.getOrCreateEphemeralChannel(it).addNote(draftEventWrap, null) + } + } + is ChannelMessageEvent -> { + rumor.channelId()?.let { channelId -> + cache.checkGetOrCreatePublicChatChannel(channelId)?.addNote(draftEventWrap, null) + } + } + is LiveActivitiesChatMessageEvent -> { + rumor.activityAddress()?.let { channelId -> + cache.getOrCreateLiveChannel(channelId).addNote(draftEventWrap, null) + } + } + is TextNoteEvent -> { + val replyTo = cache.computeReplyTo(rumor) + replyTo.forEach { it.addReply(draftEventWrap) } + } + } + } + + fun deindexDraftAsRealEvent( + draftEventWrap: Note, + rumor: Event, + ) { + when (rumor) { + is PrivateDmEvent -> { + if (rumor.canDecrypt(account.signer.pubKey)) { + val talkingWith = rumor.chatroomKey(account.signer.pubKey) + account.chatroomList.addMessage(talkingWith, draftEventWrap) + } + } + is ChatMessageEvent -> { + if (rumor.isIncluded(account.signer.pubKey)) { + val key = rumor.chatroomKey(account.signer.pubKey) + account.chatroomList.removeMessage(key, draftEventWrap) + } + } + is ChatMessageEncryptedFileHeaderEvent -> { + if (rumor.isIncluded(account.signer.pubKey)) { + val key = rumor.chatroomKey(account.signer.pubKey) + account.chatroomList.removeMessage(key, draftEventWrap) + } + } + is ChannelMessageEvent -> { + rumor.channelId()?.let { channelId -> + cache.getPublicChatChannelIfExists(channelId)?.removeNote(draftEventWrap) + } + } + is EphemeralChatEvent -> { + rumor.roomId()?.let { + cache.getEphemeralChatChannelIfExists(it)?.removeNote(draftEventWrap) + } + } + is LiveActivitiesChatMessageEvent -> { + rumor.activityAddress()?.let { channelId -> + cache.getLiveActivityChannelIfExists(channelId)?.removeNote(draftEventWrap) + } + } + is TextNoteEvent -> { + val replyTo = cache.computeReplyTo(rumor) + replyTo.forEach { it.removeReply(draftEventWrap) } + } + } + } + + suspend fun runNew(newNotes: Set) { + try { + newNotes.forEach { + consume(it) + } + + val toDelete = + newNotes.mapNotNull { + val noteEvent = it.event + if (noteEvent is DraftEvent && noteEvent.isDeleted()) { + it + } else { + null + } + } + + if (toDelete.isNotEmpty()) { + Log.w("PrecacheNewNotesProcessor", "Deleting ${toDelete.size} draft notes that should have been deleted already") + account.delete(toDelete) + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("PrecacheNewNotesProcessor", "This shouldn't happen", e) + } + } + + suspend fun runDeleted(newNotes: Set) { + try { + newNotes.forEach { + delete(it) + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("PrecacheNewNotesProcessor", "This shouldn't happen", e) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index c7af1a2d52..39312e94d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.app.Activity +import android.content.ActivityNotFoundException import android.content.Intent import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult @@ -50,7 +51,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.Discove import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription import com.vitorpamplona.quartz.nip55AndroidSigner.client.IActivityLauncher -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -184,13 +184,12 @@ private fun ListenToExternalSignerIfNeeded(accountViewModel: AccountViewModel) { val launcher: (Intent) -> Unit = { intent -> try { launcher.launch(intent) - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("Signer", "Error opening Signer app", e) + } catch (e: ActivityNotFoundException) { accountViewModel.toastManager.toast( R.string.error_opening_external_signer, R.string.error_opening_external_signer_description, ) + throw e } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/PrecacheNewNotesProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/PrecacheNewNotesProcessor.kt deleted file mode 100644 index 6b56353260..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/PrecacheNewNotesProcessor.kt +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn - -import android.util.Log -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip37Drafts.DraftEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent -import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent -import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent -import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import kotlinx.coroutines.CancellationException - -class PrecacheNewNotesProcessor( - val account: Account, - val cache: LocalCache, -) { - fun consume(note: Note) { - val noteEvent = note.event - if (noteEvent != null) { - try { - consumeAlreadyVerified(noteEvent, note) - } catch (e: Exception) { - Log.e("PrecacheNewNotesProcessor", "Error processing note", e) - } - } - } - - fun consumeAlreadyVerified( - event: Event, - publicNote: Note, - ) { - when (event) { - is OtsEvent -> { - // verifies new OTS upon arrival - Amethyst.instance.otsVerifCache.cacheVerify(event, account::otsResolver) - } - - is PrivateOutboxRelayListEvent -> { - if (event.cachedPrivateTags() == null && event.pubKey == account.signer.pubKey) { - event.privateTags(account.signer) { } - } - } - - is DraftEvent -> { - // Avoid decrypting over and over again if the event already exist. - if (event.pubKey == account.signer.pubKey) { - if (!event.isDeleted()) { - if (event.preCachedDraft(account.signer) == null) { - event.cachedDraft(account.signer) { - cache.indexDraftAsRealEvent(event, it) - } - } - } - } - } - - is GiftWrapEvent -> { - if (event.recipientPubKey() == account.signer.pubKey) { - val inner = event.innerEventId - if (inner == null) { - event.unwrap(account.signer) { - // clear the encrypted payload to save memory - cache.getOrCreateNote(event.id).event = event.copyNoContent() - if (cache.justConsume(it, null, false)) { - cache.copyRelaysFromTo(publicNote, it) - consumeAlreadyVerified(it, publicNote) - } - } - } else { - cache.copyRelaysFromTo(publicNote, inner) - val event = cache.getOrCreateNote(inner).event - if (event != null) { - consumeAlreadyVerified(event, publicNote) - } - } - } - } - - is SealedRumorEvent -> { - val inner = event.innerEventId - if (inner == null) { - event.unseal(account.signer) { - // clear the encrypted payload to save memory - cache.getOrCreateNote(event.id).event = event.copyNoContent() - - // avoid verifying these events. - cache.justConsume(it, null, true) - cache.copyRelaysFromTo(publicNote, it) - } - } else { - cache.copyRelaysFromTo(publicNote, inner) - } - } - - is LnZapEvent -> { - event.zapRequest?.let { req -> - if (req.cachedPrivateZap() == null && req.isPrivateZap()) { - // We can't know which account this was for without going through it. - req.decryptPrivateZap(account.signer) {} - } - } - } - - is LnZapRequestEvent -> { - if (event.cachedPrivateZap() == null && event.isPrivateZap()) { - event.decryptPrivateZap(account.signer) { } - } - } - } - } - - fun run(newNotes: Set) { - try { - newNotes.forEach { - consume(it) - } - - val toDelete = - newNotes.mapNotNull { - val noteEvent = it.event - if (noteEvent is DraftEvent && noteEvent.isDeleted()) { - it - } else { - null - } - } - - if (toDelete.isNotEmpty()) { - Log.w("PrecacheNewNotesProcessor", "Deleting ${toDelete.size} draft notes that should have been deleted already") - account.delete(toDelete) - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e("PrecacheNewNotesProcessor", "This shouldn't happen", e) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt index dd013814e1..ed958beadb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/BookmarkListScreen.kt @@ -67,9 +67,10 @@ fun BookmarkListScreen( factory = BookmarkPrivateFeedViewModel.Factory(accountViewModel.account), ) - val userState by accountViewModel.account.decryptBookmarks.collectAsStateWithLifecycle(null) + val bookmarkState by accountViewModel.account.bookmarkState.bookmarks + .collectAsStateWithLifecycle(null) - LaunchedEffect(userState) { + LaunchedEffect(bookmarkState) { publicFeedViewModel.invalidateData() privateFeedViewModel.invalidateData() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt index 6aa7fdd261..85cdc1d776 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPrivateFeedFilter.kt @@ -21,31 +21,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.FeedFilter class BookmarkPrivateFeedFilter( val account: Account, ) : FeedFilter() { - override fun feedKey(): String = account.userProfile().latestBookmarkList?.id ?: "" + override fun feedKey(): String = + account.bookmarkState.bookmarks.value + .hashCode() + .toString() - override fun feed(): List { - val bookmarks = account.userProfile().latestBookmarkList - - if (!account.isWriteable()) return emptyList() - - val privateTags = bookmarks?.cachedPrivateTags() ?: return emptyList() - - return privateTags - .mapNotNull { - if (it.size > 1 && it[0] == "e") { - LocalCache.checkGetOrCreateNote(it[1]) - } else if (it.size > 1 && it[0] == "a") { - LocalCache.checkGetOrCreateAddressableNote(it[1]) - } else { - null - } - }.reversed() - } + override fun feed(): List = account.bookmarkState.bookmarks.value.private } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt index 3b2925007b..34a3962fb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/bookmarks/dal/BookmarkPublicFeedFilter.kt @@ -21,28 +21,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.FeedFilter class BookmarkPublicFeedFilter( val account: Account, ) : FeedFilter() { - override fun feedKey(): String = account.userProfile().latestBookmarkList?.id ?: "" + override fun feedKey(): String = + account.bookmarkState.bookmarks.value + .hashCode() + .toString() - override fun feed(): List { - val bookmarks = account.userProfile().latestBookmarkList - - return bookmarks - ?.tags - ?.mapNotNull { - if (it.size > 1 && it[0] == "e") { - LocalCache.checkGetOrCreateNote(it[1]) - } else if (it.size > 1 && it[0] == "a") { - LocalCache.checkGetOrCreateAddressableNote(it[1]) - } else { - null - } - }?.reversed() ?: emptyList() - } + override fun feed(): List = account.bookmarkState.bookmarks.value.public } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt index 79bb6ca192..28a4156c42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomScreen.kt @@ -21,79 +21,37 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RenderRoomTopBar -import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun ChatroomScreen( - roomId: String?, + roomId: ChatroomKey, draftMessage: String? = null, replyToNote: HexKey? = null, editFromDraft: HexKey? = null, accountViewModel: AccountViewModel, nav: INav, ) { - if (roomId == null) return - DisappearingScaffold( isInvertedLayout = true, topBar = { - RoomTopBar(roomId, accountViewModel, nav) + RenderRoomTopBar(roomId, accountViewModel, nav) }, accountViewModel = accountViewModel, ) { Column(Modifier.padding(it)) { - Chatroom(roomId, draftMessage, replyToNote, editFromDraft, accountViewModel, nav) - } - } -} - -@Composable -private fun RoomTopBar( - id: String, - accountViewModel: AccountViewModel, - nav: INav, -) { - LoadRoom(roomId = id, accountViewModel) { room -> - if (room != null) { - RenderRoomTopBar(room, accountViewModel, nav) - } else { - Spacer(BottomTopHeight) - } - } -} - -@Composable -fun Chatroom( - roomId: String?, - draftMessage: String? = null, - replyToNote: HexKey? = null, - editFromDraft: HexKey? = null, - accountViewModel: AccountViewModel, - nav: INav, -) { - if (roomId == null) return - - LoadRoom(roomId, accountViewModel) { - it?.let { ChatroomView( - room = it, + room = roomId, draftMessage = draftMessage, replyToNote = replyToNote, editFromDraft = editFromDraft, @@ -103,28 +61,3 @@ fun Chatroom( } } } - -@Composable -fun LoadRoom( - roomId: String, - accountViewModel: AccountViewModel, - content: @Composable (ChatroomKey?) -> Unit, -) { - var room by remember(roomId) { mutableStateOf(null) } - - if (room == null) { - LaunchedEffect(key1 = roomId) { - launch(Dispatchers.IO) { - val newRoom = - accountViewModel.userProfile().privateChatrooms.keys.firstOrNull { - it.hashCode().toString() == roomId - } - if (room != newRoom) { - room = newRoom - } - } - } - } - - content(room) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt index 5f4fcf3a7d..bf5808d443 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt @@ -34,7 +34,7 @@ class ChatroomFeedFilter( override fun feedKey(): String = withUser.hashCode().toString() override fun feed(): List { - val messages = account.userProfile().privateChatrooms[withUser] ?: return emptyList() + val messages = account.chatroomList.getOrCreatePrivateChatroom(withUser) return messages.roomMessages .filter { account.isAcceptable(it) } @@ -43,7 +43,7 @@ class ChatroomFeedFilter( } override fun applyFilter(collection: Set): Set { - val messages = account.userProfile().privateChatrooms[withUser] ?: return emptySet() + val messages = account.chatroomList.getOrCreatePrivateChatroom(withUser) return collection.filter { it in messages.roomMessages && account.isAcceptable(it) }.toSet() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt index 5b6ce89bf4..ec79a82c65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt @@ -75,7 +75,12 @@ fun NewChatroomSubjectDialog( Surface { val groupName = remember { - mutableStateOf(accountViewModel.userProfile().privateChatrooms[room]?.subject ?: "") + mutableStateOf( + accountViewModel.account.chatroomList.chatrooms + .get(room) + ?.subject + ?.value ?: "", + ) } val message = remember { mutableStateOf("") } val scope = rememberCoroutineScope() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt index 87aa3fff27..e5f0a008f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/RoomNameOnlyDisplay.kt @@ -28,8 +28,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserRoomSubject import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserShortName import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji @@ -47,7 +47,11 @@ fun RoomNameOnlyDisplay( fontWeight: FontWeight = FontWeight.Bold, accountViewModel: AccountViewModel, ) { - val roomSubject by observeUserRoomSubject(accountViewModel.userProfile(), room, accountViewModel) + // Subscribe in the LocalCache for changes that arrive in the device + val roomSubject by accountViewModel.account.chatroomList + .getOrCreatePrivateChatroom(room) + .subject + .collectAsStateWithLifecycle() CrossfadeIfEnabled(targetState = roomSubject, modifier, accountViewModel = accountViewModel) { if (!it.isNullOrBlank()) { @@ -98,7 +102,10 @@ fun RoomNameDisplay( modifier: Modifier, accountViewModel: AccountViewModel, ) { - val roomSubject by observeUserRoomSubject(accountViewModel.userProfile(), room, accountViewModel) + val roomSubject by accountViewModel.account.chatroomList + .getOrCreatePrivateChatroom(room) + .subject + .collectAsStateWithLifecycle() CrossfadeIfEnabled(targetState = roomSubject, modifier, accountViewModel = accountViewModel) { if (!it.isNullOrBlank()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index c1f73520ba..2001c2969c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -40,7 +40,6 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger -import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState @@ -105,6 +104,9 @@ class ChatNewMessageViewModel : IZapRaiser { val draftTag = DraftTagState() + lateinit var accountViewModel: AccountViewModel + lateinit var account: Account + init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { @@ -116,8 +118,6 @@ class ChatNewMessageViewModel : } } - var accountViewModel: AccountViewModel? = null - var account: Account? = null var room: ChatroomKey? by mutableStateOf(null) var requiresNIP17: Boolean = false @@ -127,7 +127,7 @@ class ChatNewMessageViewModel : var uploadState by mutableStateOf(null) val iMetaAttachments = IMetaAttachments() - var uploadsWaitingToBeSent by mutableStateOf>(emptyList()) + var uploadsWaitingToBeSent by mutableStateOf>(emptyList()) override var message by mutableStateOf(TextFieldValue("")) @@ -169,11 +169,11 @@ class ChatNewMessageViewModel : // NIP17 Wrapped DMs / Group messages var nip17 by mutableStateOf(false) - fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() + fun lnAddress(): String? = account.userProfile().info?.lnAddress() - fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null + fun hasLnAddress(): Boolean = account.userProfile().info?.lnAddress() != null - fun user(): User? = account?.userProfile() + fun user(): User? = account.userProfile() fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM @@ -189,7 +189,7 @@ class ChatNewMessageViewModel : this.uploadState = ChatFileUploadState( - account?.settings?.defaultFileServer ?: DEFAULT_MEDIA_SERVERS[0], + account.settings.defaultFileServer, ) } @@ -231,7 +231,6 @@ class ChatNewMessageViewModel : urlPreviews.update(message) // creates a split with that author. - val accountViewModel = accountViewModel ?: return val quotedAuthor = quote.author ?: return if (quotedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { @@ -255,14 +254,12 @@ class ChatNewMessageViewModel : if (noteEvent is DraftEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { - accountViewModel?.createTempDraftNote(noteEvent) { innerNote -> - if (innerNote != null) { - val oldTag = (draft.event as? AddressableEvent)?.dTag() - if (oldTag != null) { - draftTag.set(oldTag) - } - loadFromDraft(innerNote) + accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> + val oldTag = (draft.event as? AddressableEvent)?.dTag() + if (oldTag != null) { + draftTag.set(oldTag) } + loadFromDraft(innerNote) } } } @@ -270,20 +267,19 @@ class ChatNewMessageViewModel : private fun loadFromDraft(draft: Note) { val draftEvent = draft.event ?: return - val accountViewModel = accountViewModel ?: return - val localfowardZapTo = draftEvent.tags.zapSplitSetup() - val totalWeight = localfowardZapTo.sumOf { it.weight } + val localForwardZapTo = draftEvent.tags.zapSplitSetup() + val totalWeight = localForwardZapTo.sumOf { it.weight } forwardZapTo.value = SplitBuilder() - localfowardZapTo.forEach { + localForwardZapTo.forEach { if (it is ZapSplitSetup) { val user = LocalCache.getOrCreateUser(it.pubKeyHex) forwardZapTo.value.addItem(user, (it.weight / totalWeight).toFloat()) } - // don't support edditing old-style splits. + // don't support editing old-style splits. } forwardZapToEditting.value = TextFieldValue("") - wantsForwardZapTo = localfowardZapTo.isNotEmpty() + wantsForwardZapTo = localForwardZapTo.isNotEmpty() wantsToMarkAsSensitive = draftEvent.isSensitive() @@ -324,8 +320,8 @@ class ChatNewMessageViewModel : } } } else if (draftEvent is PrivateDmEvent) { - val recepientNpub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() } - toUsers = TextFieldValue("@$recepientNpub") + val recipientNPub = draftEvent.verifiedRecipientPubKey()?.let { Hex.decode(it).toNpub() } + toUsers = TextFieldValue("@$recipientNPub") val replyId = draftEvent.replyTo() if (replyId != null) { @@ -337,7 +333,7 @@ class ChatNewMessageViewModel : message = if (draftEvent is PrivateDmEvent) { - TextFieldValue(draftEvent.cachedContentFor(accountViewModel.account.signer) ?: "") + TextFieldValue(accountViewModel.account.privateDMDecryptionCache.cachedDM(draftEvent) ?: "") } else { TextFieldValue(draftEvent.content) } @@ -358,13 +354,13 @@ class ChatNewMessageViewModel : suspend fun sendPostSync() { innerSendPost(null) - accountViewModel?.deleteDraft(draftTag.current) + accountViewModel.deleteDraft(draftTag.current) cancel() } suspend fun sendDraftSync() { if (message.text.isBlank()) { - account?.deleteDraft(draftTag.current) + account.deleteDraft(draftTag.current) } else { innerSendPost(draftTag.current) } @@ -387,20 +383,21 @@ class ChatNewMessageViewModel : context: Context, onceUploaded: () -> Unit, ) { - val account = account ?: return val uploadState = uploadState ?: return - if (nip17) { - ChatFileUploader(account).justUploadNIP17(uploadState, viewModelScope, onError, context) { - uploadsWaitingToBeSent += it - draftTag.newVersion() - onceUploaded() - } - } else { - ChatFileUploader(account).justUploadNIP04(uploadState, viewModelScope, onError, context) { - uploadsWaitingToBeSent += it - draftTag.newVersion() - onceUploaded() + accountViewModel.runIOCatching { + if (nip17) { + ChatFileUploader(account).justUploadNIP17(uploadState, onError, context) { + uploadsWaitingToBeSent += it + draftTag.newVersion() + onceUploaded() + } + } else { + ChatFileUploader(account).justUploadNIP04(uploadState, onError, context) { + uploadsWaitingToBeSent += it + draftTag.newVersion() + onceUploaded() + } } } } @@ -411,27 +408,27 @@ class ChatNewMessageViewModel : onceUploaded: () -> Unit, ) { val room = room ?: return - val account = account ?: return val uploadState = uploadState ?: return - if (nip17) { - ChatFileUploader(account).justUploadNIP17(uploadState, viewModelScope, onError, context) { - ChatFileSender(room, account).sendNIP17(it) - draftTag.newVersion() - onceUploaded() - } - } else { - ChatFileUploader(account).justUploadNIP04(uploadState, viewModelScope, onError, context) { - ChatFileSender(room, account).sendNIP04(it) - draftTag.newVersion() - onceUploaded() + accountViewModel.runIOCatching { + if (nip17) { + ChatFileUploader(account).justUploadNIP17(uploadState, onError, context) { + ChatFileSender(room, account).sendNIP17(it) + draftTag.newVersion() + onceUploaded() + } + } else { + ChatFileUploader(account).justUploadNIP04(uploadState, onError, context) { + ChatFileSender(room, account).sendNIP04(it) + draftTag.newVersion() + onceUploaded() + } } } } - private fun innerSendPost(dTag: String?) { + private suspend fun innerSendPost(dTag: String?) { val room = room ?: return - val accountViewModel = accountViewModel ?: return val urls = findURLs(message.text) val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet()) @@ -536,12 +533,6 @@ class ChatNewMessageViewModel : draftTag.rotate() } - fun deleteDraft() { - viewModelScope.launch(Dispatchers.IO) { - accountViewModel?.deleteDraft(draftTag.current) - } - } - fun addToMessage(it: String) { updateMessage(TextFieldValue(message.text + " " + it)) } @@ -578,7 +569,7 @@ class ChatNewMessageViewModel : fun updateRoomFromUsersInput() { viewModelScope.launch(Dispatchers.Default) { delay(300) - val toUsersTagger = NewMessageTagger(toUsers.text, null, null, accountViewModel!!) + val toUsersTagger = NewMessageTagger(toUsers.text, null, null, accountViewModel) toUsersTagger.run() val users = toUsersTagger.pTags?.mapTo(mutableSetOf()) { it.pubkeyHex } @@ -648,10 +639,9 @@ class ChatNewMessageViewModel : val wordToInsert = item.link.url + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare( - item.link.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, - ) + iMetaAttachments.downloadAndPrepare(item.link.url) { + Amethyst.instance.okHttpClients.getHttpClient(accountViewModel.account.shouldUseTorForImageDownload(item.link.url)) + } } message = message.replaceCurrentWord(wordToInsert) @@ -701,7 +691,7 @@ class ChatNewMessageViewModel : override fun updateZapFromText() { viewModelScope.launch(Dispatchers.Default) { - val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!) + val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel) tagger.run() tagger.pTags?.forEach { taggedUser -> if (!forwardZapTo.value.items.any { it.key == taggedUser }) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index 6d78249197..c47094f166 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -55,7 +55,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -127,6 +126,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @Composable @@ -152,10 +152,8 @@ fun NewGroupDMScreen( val context = LocalContext.current - val scope = rememberCoroutineScope() - LaunchedEffect(Unit) { - launch(Dispatchers.IO) { + withContext(Dispatchers.IO) { message?.ifBlank { null }?.let { postViewModel.updateMessage(TextFieldValue(it)) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt index 8d22aeeb62..344fc1bf55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileSender.kt @@ -34,7 +34,7 @@ class ChatFileSender( val chatroom: ChatroomKey, val account: Account, ) { - fun sendNIP17(uploads: List) { + suspend fun sendNIP17(uploads: List) { uploads.forEach { if (it.cipher != null) { sendNIP17(it.result, it.caption, it.contentWarningReason, it.cipher) @@ -42,7 +42,7 @@ class ChatFileSender( } } - fun sendNIP17( + suspend fun sendNIP17( result: UploadOrchestrator.OrchestratorResult.ServerResult, caption: String?, contentWarningReason: String?, @@ -73,7 +73,7 @@ class ChatFileSender( // NIP 04 // ------ - fun sendNIP04(uploads: List) { + suspend fun sendNIP04(uploads: List) { uploads.forEach { if (it.cipher == null) { sendNIP04(it.result, it.caption, it.contentWarningReason) @@ -81,7 +81,7 @@ class ChatFileSender( } } - fun sendNIP04( + suspend fun sendNIP04( result: UploadOrchestrator.OrchestratorResult.ServerResult, caption: String?, contentWarningReason: String?, @@ -103,7 +103,7 @@ class ChatFileSender( ) } - fun sendAll(uploads: List) { + suspend fun sendAll(uploads: List) { uploads.forEach { if (it.cipher != null) { sendNIP17(it.result, it.caption, it.contentWarningReason, it.cipher) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index 7471ba8671..db5c43d959 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -28,9 +28,6 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip17Dm.files.encryption.AESGCM -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch class ChatFileUploader( val account: Account, @@ -39,100 +36,90 @@ class ChatFileUploader( // NIP 17 // ------ - fun justUploadNIP17( + suspend fun justUploadNIP17( viewState: ChatFileUploadState, - scope: CoroutineScope, onError: (title: String, message: String) -> Unit, context: Context, - onceUploaded: (List) -> Unit, + onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return + viewState.isUploadingImage = true - scope.launch(Dispatchers.Default) { - viewState.isUploadingImage = true + val cipher = AESGCM() - val cipher = AESGCM() + val results = + orchestrator.uploadEncrypted( + viewState.caption, + viewState.contentWarningReason, + MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), + cipher, + viewState.selectedServer, + account, + context, + ) - val results = - orchestrator.uploadEncrypted( - scope, - viewState.caption, - viewState.contentWarningReason, - MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), - cipher, - viewState.selectedServer, - account, - context, - ) - - if (results.allGood) { - val list = - results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, cipher) - } else { - null - } + if (results.allGood) { + val list = + results.successful.mapNotNull { state -> + if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, cipher) + } else { + null } + } - onceUploaded(list) - viewState.reset() - } else { - val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct() + onceUploaded(list) + viewState.reset() + } else { + val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct() - onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) - } - - viewState.isUploadingImage = false + onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } + + viewState.isUploadingImage = false } // ------ // NIP 04 // ------ - fun justUploadNIP04( + suspend fun justUploadNIP04( viewState: ChatFileUploadState, - scope: CoroutineScope, onError: (title: String, message: String) -> Unit, context: Context, - onceUploaded: (List) -> Unit, + onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return + viewState.isUploadingImage = true - scope.launch(Dispatchers.Default) { - viewState.isUploadingImage = true + val results = + orchestrator.upload( + viewState.caption, + viewState.contentWarningReason, + MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), + viewState.selectedServer, + account, + context, + ) - val results = - orchestrator.upload( - scope, - viewState.caption, - viewState.contentWarningReason, - MediaCompressor.intToCompressorQuality(viewState.mediaQualitySlider), - viewState.selectedServer, - account, - context, - ) - - if (results.allGood) { - val list = - results.successful.mapNotNull { state -> - if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, null) - } else { - null - } + if (results.allGood) { + val list = + results.successful.mapNotNull { state -> + if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + SuccessfulUploads(state.result, viewState.caption, viewState.contentWarningReason, null) + } else { + null } + } - onceUploaded(list) - viewState.reset() - } else { - val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct() + onceUploaded(list) + viewState.reset() + } else { + val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct() - onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) - } - - viewState.isUploadingImage = false + onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } + + viewState.isUploadingImage = false } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index b923e5812c..0df3d24505 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata +import android.graphics.Picture import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -55,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.CreatingTopBar import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelName import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog @@ -67,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions @Composable fun ChannelMetadataScreen( @@ -133,10 +136,17 @@ private fun ChannelMetadataScaffold( nav.popBack() }, onPost = { - postViewModel.createOrUpdate { - nav.nav(routeFor(it)) + try { + postViewModel.createOrUpdate { + nav.nav(routeFor(it)) + } + nav.popBack() + } catch (e: SignerExceptions.ReadOnlyException) { + accountViewModel.toastManager.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_sign_events, + ) } - nav.popBack() }, ) } else { @@ -148,8 +158,15 @@ private fun ChannelMetadataScaffold( nav.popBack() }, onPost = { - postViewModel.createOrUpdate { } - nav.popBack() + try { + postViewModel.createOrUpdate { } + nav.popBack() + } catch (e: SignerExceptions.ReadOnlyException) { + accountViewModel.toastManager.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_sign_events, + ) + } }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index ff1c0ab197..403dc47a4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySet import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent @@ -105,16 +106,15 @@ class ChannelMetadataViewModel : ViewModel() { channelRelays.value.map { it.relay }, ) - account.signAndSendPrivatelyOrBroadcast( - template, - relayList = { it.channelInfo().relays }, - onDone = { - val channel = LocalCache.getOrCreatePublicChatChannel(it.id) - // follows the channel - account.follow(channel) - onDone(channel) - }, - ) + val signedResult = + account.signAndSendPrivatelyOrBroadcast( + template, + relayList = { it.channelInfo().relays }, + ) + val channel = LocalCache.getOrCreatePublicChatChannel(signedResult.id) + // follows the channel + account.follow(channel) + onDone(channel) } else { val event = channel.event @@ -141,14 +141,13 @@ class ChannelMetadataViewModel : ViewModel() { ) } - account.signAndSendPrivatelyOrBroadcast( - template, - relayList = { it.channelInfo().relays }, - onDone = { - val channel = LocalCache.getOrCreatePublicChatChannel(it.id) - onDone(channel) - }, - ) + val signedResult = + account.signAndSendPrivatelyOrBroadcast( + template, + relayList = { it.channelInfo().relays }, + ) + val channel = LocalCache.getOrCreatePublicChatChannel(signedResult.id) + onDone(channel) } } @@ -237,6 +236,9 @@ class ChannelMetadataViewModel : ViewModel() { onUploading(false) onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading)) } + } catch (_: SignerExceptions.ReadOnlyException) { + onUploading(false) + onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.login_with_a_private_key_to_be_able_to_upload)) } catch (e: Exception) { if (e is CancellationException) throw e onUploading(false) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 152b76e2a8..0468e54346 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -67,6 +67,7 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash @@ -196,14 +197,12 @@ open class ChannelNewMessageViewModel : if (noteEvent is DraftEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { - accountViewModel?.createTempDraftNote(noteEvent) { innerNote -> - if (innerNote != null) { - val oldTag = (draft.event as? AddressableEvent)?.dTag() - if (oldTag != null) { - draftTag.set(oldTag) - } - loadFromDraft(innerNote) + accountViewModel?.createTempDraftNote(noteEvent)?.let { innerNote -> + val oldTag = (draft.event as? AddressableEvent)?.dTag() + if (oldTag != null) { + draftTag.set(oldTag) } + loadFromDraft(innerNote) } } } @@ -301,6 +300,19 @@ open class ChannelNewMessageViewModel : onError: (title: String, message: String) -> Unit, context: Context, onceUploaded: () -> Unit, + ) = try { + uploadUnsafe(onError, context, onceUploaded) + } catch (e: SignerExceptions.ReadOnlyException) { + onError( + stringRes(context, R.string.read_only_user), + stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events), + ) + } + + fun uploadUnsafe( + onError: (title: String, message: String) -> Unit, + context: Context, + onceUploaded: () -> Unit, ) { viewModelScope.launch(Dispatchers.Default) { val myAccount = account ?: return@launch @@ -312,7 +324,6 @@ open class ChannelNewMessageViewModel : val results = myMultiOrchestrator.upload( - viewModelScope, uploadState.caption, uploadState.contentWarningReason, MediaCompressor.intToCompressorQuality(uploadState.mediaQualitySlider), @@ -324,16 +335,15 @@ open class ChannelNewMessageViewModel : if (results.allGood) { results.successful.forEach { if (it.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - account?.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, uploadState.caption, uploadState.contentWarningReason) { nip95 -> - nip95attachments = nip95attachments + nip95 - val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } + val nip95 = myAccount.createNip95(it.result.bytes, headerInfo = it.result.fileHeader, uploadState.caption, uploadState.contentWarningReason) + nip95attachments = nip95attachments + nip95 + val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } - note?.let { - message = message.insertUrlAtCursor(it.toNostrUri()) - } - - urlPreview = findUrlInMessage() + note?.let { + message = message.insertUrlAtCursor(it.toNostrUri()) } + + urlPreview = findUrlInMessage() } else if (it.result is UploadOrchestrator.OrchestratorResult.ServerResult) { iMetaAttachments.add(it.result, uploadState.caption, uploadState.contentWarningReason) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 7207192f72..c3c3cb9868 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -275,7 +275,7 @@ private fun UserRoomCompose( NewItemsBubble() } }, - onClick = { nav.nav(Route.Room(room.hashCode())) }, + onClick = { nav.nav(Route.Room(room)) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index 4b35b61baa..b59fbe5d8e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -39,28 +39,27 @@ class ChatroomListKnownFeedFilter( // returns the last Note of each user. override fun feed(): List { - val me = account.userProfile() + val chatList = account.chatroomList val followingKeySet = account.followingKeySet() - val knownChatrooms = - me.privateChatrooms.filter { - (it.value.senderIntersects(followingKeySet) || me.hasSentMessagesTo(it.key)) && - !account.isAllHidden(it.key.users) - } - val privateMessages = - knownChatrooms.mapNotNull { it -> - it.value.roomMessages.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).lastOrNull { - it.event != null + chatList.chatrooms.mapNotNull { key, chatroom -> + if ((chatroom.senderIntersects(followingKeySet) || chatList.hasSentMessagesTo(key)) && + !account.isAllHidden(key.users) + ) { + chatroom.lastMessage + } else { + null } } val publicChannels = account .publicChatList.flow.value - .mapNotNull { LocalCache.getPublicChatChannelIfExists(it.eventId) } .mapNotNull { it -> - it.notes + LocalCache + .getOrCreatePublicChatChannel(it.eventId) + .notes .filter { key, it -> account.isAcceptable(it) && it.event != null } .sortedWith(DefaultFeedOrder) .firstOrNull() @@ -69,9 +68,10 @@ class ChatroomListKnownFeedFilter( val ephemeralChats = account .ephemeralChatList.liveEphemeralChatList.value - .mapNotNull { LocalCache.getEphemeralChatChannelIfExists(it) } .mapNotNull { it -> - it.notes + LocalCache + .getOrCreateEphemeralChannel(it) + .notes .filter { key, it -> account.isAcceptable(it) && it.event != null } .sortedWith(DefaultFeedOrder) .firstOrNull() @@ -229,13 +229,13 @@ class ChatroomListKnownFeedFilter( .forEach { newNote -> val roomKey = (newNote.event as? ChatroomKeyable)?.chatroomKey(me.pubkeyHex) if (roomKey != null) { - val room = account.userProfile().privateChatrooms[roomKey] + val room = account.chatroomList.chatrooms.get(roomKey) if (room != null) { if ( ( newNote.author?.pubkeyHex == me.pubkeyHex || room.senderIntersects(followingKeySet) || - me.hasSentMessagesTo(roomKey) + account.chatroomList.hasSentMessagesTo(roomKey) ) && !account.isAllHidden(roomKey.users) ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt index 6c9d1fea03..a7b1e5a336 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable @@ -36,20 +35,15 @@ class ChatroomListNewFeedFilter( // returns the last Note of each user. override fun feed(): List { - val me = account.userProfile() + val chatList = account.chatroomList val followingKeySet = account.followingKeySet() - val newChatrooms = - me.privateChatrooms.filter { - !it.value.senderIntersects(followingKeySet) && - !me.hasSentMessagesTo(it.key) && - !account.isAllHidden(it.key.users) - } - val privateMessages = - newChatrooms.mapNotNull { it -> - it.value.roomMessages.sortedWith(DefaultFeedOrder).firstOrNull { - it.event != null + chatList.chatrooms.mapNotNull { key, chatroom -> + if (!chatroom.senderIntersects(followingKeySet) && !chatList.hasSentMessagesTo(key) && !account.isAllHidden(key.users)) { + chatroom.lastMessage + } else { + null } } @@ -110,19 +104,17 @@ class ChatroomListNewFeedFilter( val followingKeySet = account.followingKeySet() val newRelevantPrivateMessages = mutableMapOf() - newItems - .filter { it.event is PrivateDmEvent } - .forEach { newNote -> - val roomKey = (newNote.event as? ChatroomKeyable)?.chatroomKey(me.pubkeyHex) - val room = account.userProfile().privateChatrooms[roomKey] + newItems.forEach { newNote -> + val noteEvent = newNote.event + if (noteEvent is ChatroomKeyable) { + val roomKey = noteEvent.chatroomKey(me.pubkeyHex) + val room = account.chatroomList.chatrooms.get(roomKey) - if ( - roomKey != null && - room != null && + if (room != null && ( newNote.author?.pubkeyHex != me.pubkeyHex && room.senderIntersects(followingKeySet) && - !me.hasSentMessagesTo(roomKey) + !account.chatroomList.hasSentMessagesTo(roomKey) ) && !account.isAllHidden(roomKey.users) ) { @@ -136,6 +128,8 @@ class ChatroomListNewFeedFilter( } } } + } + return newRelevantPrivateMessages } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt index 56c1e1e331..c3048c143d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/MessagesTwoPane.kt @@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.topbars.AmethystClickableIcon import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.Chatroom +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelFabColumn import com.vitorpamplona.amethyst.ui.theme.Size20dp @@ -104,9 +104,12 @@ fun MessagesTwoPane( Box(Modifier.fillMaxSize().systemBarsPadding()) { twoPaneNav.innerNav.value?.let { if (it is Route.Room) { - Chatroom( - roomId = it.id.toString(), + ChatroomView( + room = it.toKey(), accountViewModel = accountViewModel, + draftMessage = it.message, + replyToNote = it.replyId, + editFromDraft = it.draftId, nav = nav, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt index c9ed3da56b..2f87c20798 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/ChannelCardCompose.kt @@ -45,7 +45,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfPadding import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt index 30d7da9c10..dfccfe8183 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/DiscoverScreen.kt @@ -83,7 +83,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt index 56140e4d4a..e1c5b66ab1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent open class DiscoverLongFormFeedFilter( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt index 039fddc9a4..1fe527ce0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent open class DiscoverChatFeedFilter( val account: Account, @@ -103,7 +103,7 @@ open class DiscoverChatFeedFilter( override fun sort(collection: Set): List { val lastNote = collection.associateWith { note -> - LocalCache.getPublicChatChannelIfExists(note.idHex)?.lastNoteCreatedAt ?: 0 + LocalCache.getPublicChatChannelIfExists(note.idHex)?.lastNote?.createdAt() ?: 0 } return collection diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt index d1a9c0d809..07d278ff2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/DiscoverFollowSetsFeedFilter.kt @@ -26,9 +26,9 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent open class DiscoverFollowSetsFeedFilter( val account: Account, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt index f186b54fa8..dda0079c93 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/FollowSetCard.kt @@ -62,7 +62,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -85,13 +85,13 @@ fun RenderFollowSetThumb( val noteEvent = it.event as? FollowListEvent FollowSetCard( - name = noteEvent?.nameOrTitle()?.ifBlank { null } ?: noteEvent?.dTag() ?: "", + name = noteEvent?.title()?.ifBlank { null } ?: noteEvent?.dTag() ?: "", media = noteEvent?.image()?.ifBlank { null }, description = noteEvent?.description(), users = accountViewModel .loadUsersSync( - noteEvent?.pubKeys() ?: emptyList(), + noteEvent?.followIds() ?: emptyList(), ).toImmutableList(), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt index 7616138712..745746ba8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassem import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent fun filterFollowSetsAllCommunities( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt index 794c18d8aa..9c657837db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt @@ -27,7 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent fun filterFollowSetsAuthors( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt index cb38081b8a..47c061454b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent fun filterFollowSetsByCommunity( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt index 578a766d25..236a58e3a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent fun filterFollowSetsByGeohash( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt index 8e330a8625..aa7d74d43f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtagAlts -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent fun filterFollowSetsByHashtag( relay: NormalizedRelayUrl, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt index 17223c80c1..78b8ed8d7f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt @@ -24,7 +24,7 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayF import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent fun filterFollowSetsGlobal( relays: GlobalTopNavPerRelayFilterSet, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt index 2f711d7038..3c60054a10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt @@ -30,8 +30,8 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCo import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.tags.StatusTag diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt index 0ee01359de..771da9c840 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt index f790a49ede..d2f3e9a7b4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt index 2f62564a0c..038c2bbc92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt @@ -25,8 +25,8 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent open class DiscoverMarketplaceFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 9bb7e67be8..25f64e65f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview @@ -141,17 +142,28 @@ fun NewProductScreen( titleRes = R.string.new_product, isActive = postViewModel::canPost, onCancel = { - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendDraftSync() - nav.popBack() - postViewModel.cancel() + try { + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.sendDraftSync() + nav.popBack() + postViewModel.cancel() + } + } catch (e: SignerExceptions.ReadOnlyException) { + // do nothing. } }, onPost = { - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - postViewModel.sendPostSync() - nav.popBack() - postViewModel.cancel() + try { + accountViewModel.viewModelScope.launch(Dispatchers.IO) { + postViewModel.sendPostSync() + nav.popBack() + postViewModel.cancel() + } + } catch (e: SignerExceptions.ReadOnlyException) { + accountViewModel.toastManager.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_sign_events, + ) } }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index cce40d4b03..b1108a34ca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -194,14 +194,12 @@ open class NewProductViewModel : if (noteEvent is DraftEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { - accountViewModel.createTempDraftNote(noteEvent) { innerNote -> - if (innerNote != null) { - val oldTag = (draft.event as? AddressableEvent)?.dTag() - if (oldTag != null) { - draftTag.set(oldTag) - } - loadFromDraft(innerNote) + accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> + val oldTag = (draft.event as? AddressableEvent)?.dTag() + if (oldTag != null) { + draftTag.set(oldTag) } + loadFromDraft(innerNote) } } } @@ -387,7 +385,6 @@ open class NewProductViewModel : val results = myMultiOrchestrator.upload( - viewModelScope, alt, contentWarningReason, MediaCompressor.intToCompressorQuality(mediaQuality), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt index 85ca853cd1..1aefd5a1b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/DvmContentDiscoveryScreen.kt @@ -117,10 +117,10 @@ fun DvmContentDiscoveryScreen( DvmTopBar(appDefinitionEventId, accountViewModel, nav) }, accountViewModel = accountViewModel, - ) { - Column(Modifier.padding(it)) { - LoadNote(baseNoteHex = appDefinitionEventId, accountViewModel = accountViewModel) { - it?.let { baseNote -> + ) { paddingValues -> + Column(Modifier.padding(paddingValues)) { + LoadNote(baseNoteHex = appDefinitionEventId, accountViewModel = accountViewModel) { note -> + note?.let { baseNote -> WatchNoteEvent( baseNote, onNoteEventFound = { @@ -376,7 +376,7 @@ fun FeedDVM( if (invoice != null) { val context = LocalContext.current Button(onClick = { - if (accountViewModel.account.hasWalletConnectSetup()) { + if (accountViewModel.account.nip47SignerState.hasWalletConnectSetup()) { accountViewModel.sendZapPaymentRequestFor( bolt11 = invoice, zappedNote = null, @@ -413,7 +413,7 @@ fun FeedDVM( val amountInInvoice = try { LnInvoiceUtil.getAmountInSats(invoice).toLong() - } catch (e: Exception) { + } catch (_: Exception) { null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt index 98fc314c5a..34d52608ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/dal/NIP90ContentDiscoveryResponseFilter.kt @@ -27,8 +27,8 @@ import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent open class NIP90ContentDiscoveryResponseFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index b0a5af92ed..096b5ea761 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -75,6 +75,7 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags @@ -114,13 +115,11 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size -import com.vitorpamplona.quartz.utils.tryAndWait import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlin.coroutines.resume enum class UserSuggestionAnchor { MAIN_MESSAGE, @@ -246,14 +245,12 @@ open class ShortNotePostViewModel : if (draft != null && noteEvent is DraftEvent && noteAuthor != null) { viewModelScope.launch(Dispatchers.IO) { - accountViewModel.createTempDraftNote(noteEvent) { innerNote -> - if (innerNote != null) { - val oldTag = (draft.event as? AddressableEvent)?.dTag() - if (oldTag != null) { - draftTag.set(oldTag) - } - loadFromDraft(innerNote) + accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> + val oldTag = (draft.event as? AddressableEvent)?.dTag() + if (oldTag != null) { + draftTag.set(oldTag) } + loadFromDraft(innerNote) } } } else { @@ -478,11 +475,7 @@ open class ShortNotePostViewModel : } } - tryAndWait { continuation -> - accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) { - continuation.resume(it) - } - } + accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) accountViewModel?.deleteDraft(draftTag.current) @@ -596,6 +589,22 @@ open class ShortNotePostViewModel : server: ServerName, onError: (title: String, message: String) -> Unit, context: Context, + ) = try { + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context) + } catch (e: SignerExceptions.ReadOnlyException) { + onError( + stringRes(context, R.string.read_only_user), + stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events), + ) + } + + fun uploadUnsafe( + alt: String?, + contentWarningReason: String?, + mediaQuality: Int, + server: ServerName, + onError: (title: String, message: String) -> Unit, + context: Context, ) { viewModelScope.launch(Dispatchers.Default) { val myAccount = account ?: return@launch @@ -606,7 +615,6 @@ open class ShortNotePostViewModel : val results = myMultiOrchestrator.upload( - viewModelScope, alt, contentWarningReason, MediaCompressor.intToCompressorQuality(mediaQuality), @@ -618,14 +626,13 @@ open class ShortNotePostViewModel : if (results.allGood) { results.successful.forEach { state -> if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - account?.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) { nip95 -> - nip95attachments = nip95attachments + nip95 - val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } + val nip95 = myAccount.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) + nip95attachments = nip95attachments + nip95 + val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } - note?.let { - message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) - urlPreviews.update(message) - } + note?.let { + message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) + urlPreviews.update(message) } } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { val iMeta = @@ -656,7 +663,6 @@ open class ShortNotePostViewModel : multiOrchestrator = null } else { val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct() - onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 75f5a2d510..a76b2ac972 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -79,7 +79,7 @@ class HomeLiveFilter( val revisedOldList = oldList.filter { channel -> - channel.lastNoteCreatedAt > fiveMinsAgo + (channel.lastNote?.createdAt() ?: 0) > fiveMinsAgo } val newItemsToBeAdded = applyFilter(newItems) @@ -140,7 +140,7 @@ class HomeLiveFilter( return collection.sortedWith( compareByDescending { followCounts[it] } - .thenByDescending { it.lastNoteCreatedAt } + .thenByDescending { it.lastNote?.createdAt() ?: 0 } .thenBy { it.roomId.id } .thenBy { it.roomId.relayUrl }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index 76ec59bb32..c4b0a8d300 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -39,8 +39,8 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent @@ -99,7 +99,7 @@ class NotificationFeedFilter( val zapRequest = noteEvent.zapRequest if (zapRequest != null) { if (noteEvent.zapRequest?.isPrivateZap() == true) { - zapRequest.cachedPrivateZap()?.pubKey ?: zapRequest.pubKey + account.privateZapsDecryptionCache.cachedPrivateZap(zapRequest)?.pubKey ?: zapRequest.pubKey } else { zapRequest.pubKey } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt index 9b4584d436..dee62a42af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/dal/UserProfileBookmarksFeedFilter.kt @@ -22,12 +22,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.LocalCache.notes import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FeedFilter -import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses -import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark class UserProfileBookmarksFeedFilter( val user: User, @@ -36,22 +37,19 @@ class UserProfileBookmarksFeedFilter( override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + user.pubkeyHex override fun feed(): List { + val note = LocalCache.getOrCreateAddressableNote(BookmarkListEvent.createBookmarkAddress(user.pubkeyHex)) + val noteEvent = note.event as? BookmarkListEvent + + if (noteEvent == null) return emptyList() + val notes = - user.latestBookmarkList - ?.taggedEvents() - ?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } - ?.toSet() - ?: emptySet() + noteEvent.publicBookmarks().mapNotNull { + when (it) { + is AddressBookmark -> LocalCache.getOrCreateAddressableNote(it.address) + is EventBookmark -> LocalCache.checkGetOrCreateNote(it.eventId) + } + } - val addresses = - user.latestBookmarkList - ?.taggedAddresses() - ?.map { LocalCache.getOrCreateAddressableNote(it) } - ?.toSet() - ?: emptySet() - - return (notes + addresses) - .filter { account.isAcceptable(it) } - .sortedWith(DefaultFeedOrder) + return notes.reversed() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt index f3108d494f..948c3fa135 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLists.kt @@ -24,10 +24,10 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent -import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent val UserProfileListKinds = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt index be54c2dade..aeb22a742e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/GalleryCardCompose.kt @@ -154,7 +154,7 @@ fun ClickableNote( } else { baseNote } - routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav.nav(it) } + routeFor(redirectToNote, accountViewModel.account)?.let { nav.nav(it) } }, onLongClick = showPopup, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt index 633842a57e..2866e57668 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt @@ -28,8 +28,8 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt index 68221abeec..776bdc8c97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt @@ -109,8 +109,8 @@ fun DisplayLNAddress( onSuccess = { zapExpanded = false // pay directly - if (accountViewModel.account.hasWalletConnectSetup()) { - accountViewModel.sendZapPaymentRequestFor(it, null, onSent = {}) { response -> + if (accountViewModel.account.nip47SignerState.hasWalletConnectSetup()) { + accountViewModel.sendZapPaymentRequestFor(it, null) { response -> if (response is PayInvoiceSuccessResponse) { showInfoMessageDialog = stringRes(context, R.string.payment_successful) } else if (response is PayInvoiceErrorResponse) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt index 9c747a6125..f2a477963d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt @@ -25,20 +25,17 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeToMessage import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.ZeroPadding -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun MessageButton( @@ -46,15 +43,20 @@ fun MessageButton( accountViewModel: AccountViewModel, nav: INav, ) { - val scope = rememberCoroutineScope() - FilledTonalButton( modifier = Modifier .padding(horizontal = 3.dp) .width(50.dp), onClick = { - scope.launch(Dispatchers.IO) { accountViewModel.createChatRoomFor(user) { nav.nav(Route.Room(it)) } } + accountViewModel.createChatRoomFor(user) { chatroomKey -> + nav.nav( + routeToMessage( + room = chatroomKey, + account = accountViewModel.account, + ), + ) + } }, contentPadding = ZeroPadding, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt index 2e2ea23472..bb6e918c15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/redirect/LoadRedirectScreen.kt @@ -76,7 +76,7 @@ fun LoadRedirectScreen( val event = noteState.note.event if (event != null) { withContext(Dispatchers.IO) { - routeFor(event, accountViewModel.userProfile())?.let { route -> + routeFor(event, accountViewModel.account)?.let { route -> nav.popUpTo(route, Route.EventRedirect::class) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index afe578239f..ab81f80bc6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -84,16 +84,16 @@ fun AllRelayListScreen( val localViewModel: LocalRelayListViewModel = viewModel() val connectedViewModel: ConnectedRelayListViewModel = viewModel() - dmViewModel.init(accountViewModel.account) - nip65ViewModel.init(accountViewModel.account) - searchViewModel.init(accountViewModel.account) - localViewModel.init(accountViewModel.account) - privateOutboxViewModel.init(accountViewModel.account) - connectedViewModel.init(accountViewModel.account) - blockedViewModel.init(accountViewModel.account) - trustedViewModel.init(accountViewModel.account) + dmViewModel.init(accountViewModel) + nip65ViewModel.init(accountViewModel) + searchViewModel.init(accountViewModel) + localViewModel.init(accountViewModel) + privateOutboxViewModel.init(accountViewModel) + connectedViewModel.init(accountViewModel) + blockedViewModel.init(accountViewModel) + trustedViewModel.init(accountViewModel) - LaunchedEffect(accountViewModel.account) { + LaunchedEffect(accountViewModel) { dmViewModel.load() nip65ViewModel.load() searchViewModel.load() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt index a0bb1fa64b..77f93513b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/blocked/BlockedRelayListViewModel.kt @@ -24,9 +24,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class BlockedRelayListViewModel : BasicRelaySetupInfoModel() { - override fun getRelayList(): List? = account.blockedRelayList.getBlockedRelayList()?.relays() + override fun getRelayList(): List? = + account.blockedRelayList.flow.value + .toList() - override fun saveRelayList(urlList: List) { + override suspend fun saveRelayList(urlList: List) { account.saveBlockedRelayList(urlList) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index 37151d9c75..fabec89a73 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.replace +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -34,6 +35,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch abstract class BasicRelaySetupInfoModel : ViewModel() { + lateinit var accountViewModel: AccountViewModel lateinit var account: Account private val _relays = MutableStateFlow>(emptyList()) @@ -41,8 +43,9 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { var hasModified = false - fun init(account: Account) { - this.account = account + fun init(accountViewModel: AccountViewModel) { + this.accountViewModel = accountViewModel + this.account = accountViewModel.account } fun load() { @@ -52,11 +55,11 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { abstract fun getRelayList(): List? - abstract fun saveRelayList(urlList: List) + abstract suspend fun saveRelayList(urlList: List) fun create() { if (hasModified) { - viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { saveRelayList(_relays.value.map { it.relay }) clear() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt index 416b7a3797..9759533876 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/connected/ConnectedRelayListViewModel.kt @@ -30,6 +30,6 @@ class ConnectedRelayListViewModel : BasicRelaySetupInfoModel() { .value.available .sorted() - override fun saveRelayList(urlList: List) { + override suspend fun saveRelayList(urlList: List) { } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt index 8c7bc3cec4..3f8df123f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/AddDMRelayListDialog.kt @@ -57,9 +57,9 @@ fun AddDMRelayListDialog( ) { val postViewModel: DMRelayListViewModel = viewModel() - postViewModel.init(accountViewModel.account) + postViewModel.init(accountViewModel) - LaunchedEffect(accountViewModel.account) { + LaunchedEffect(postViewModel, accountViewModel.account) { postViewModel.load() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt index 581ed87643..01b31c931a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt @@ -26,7 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class DMRelayListViewModel : BasicRelaySetupInfoModel() { override fun getRelayList(): List? = account.dmRelayList.getDMRelayList()?.relays() - override fun saveRelayList(urlList: List) { + override suspend fun saveRelayList(urlList: List) { account.saveDMRelayList(urlList) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt index 9cabbb3bd3..496190e8eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/local/LocalRelayListViewModel.kt @@ -28,7 +28,7 @@ class LocalRelayListViewModel : BasicRelaySetupInfoModel() { account.localRelayList.flow.value .toList() - override fun saveRelayList(urlList: List) { + override suspend fun saveRelayList(urlList: List) { account.localRelayList.saveRelayList(urlList) {} } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt index c6067fcdbc..d90292c232 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt @@ -24,9 +24,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class PrivateOutboxRelayListViewModel : BasicRelaySetupInfoModel() { - override fun getRelayList(): List? = account.privateStorageRelayList.getPrivateOutboxRelayList()?.relays() + override fun getRelayList(): List? = + account.privateStorageRelayList.flow.value + .toList() - override fun saveRelayList(urlList: List) { + override suspend fun saveRelayList(urlList: List) { account.savePrivateOutboxRelayList(urlList) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index 2b5d12620b..a90f5aebed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.replace +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo @@ -39,6 +40,7 @@ import kotlinx.coroutines.launch @Stable class Nip65RelayListViewModel : ViewModel() { + private lateinit var accountViewModel: AccountViewModel private lateinit var account: Account private val _homeRelays = MutableStateFlow>(emptyList()) @@ -49,8 +51,9 @@ class Nip65RelayListViewModel : ViewModel() { var hasModified = false - fun init(account: Account) { - this.account = account + fun init(accountViewModel: AccountViewModel) { + this.accountViewModel = accountViewModel + this.account = accountViewModel.account } fun load() { @@ -60,7 +63,7 @@ class Nip65RelayListViewModel : ViewModel() { fun create() { if (hasModified) { - viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { val writes = _homeRelays.value.map { it.relay }.toSet() val reads = _notificationRelays.value.map { it.relay }.toSet() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt index 06edc49bc5..4fce997dd5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt @@ -24,9 +24,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class SearchRelayListViewModel : BasicRelaySetupInfoModel() { - override fun getRelayList(): List? = account.searchRelayList.getSearchRelayList()?.relays() + override fun getRelayList(): List? = + account.searchRelayList.flow.value + .toList() - override fun saveRelayList(urlList: List) { + override suspend fun saveRelayList(urlList: List) { account.saveSearchRelayList(urlList) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt index 6c3e4600f0..1d1f718828 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/trusted/TrustedRelayListViewModel.kt @@ -24,9 +24,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl class TrustedRelayListViewModel : BasicRelaySetupInfoModel() { - override fun getRelayList(): List? = account.trustedRelayList.getTrustedRelayList()?.relays() + override fun getRelayList(): List? = + account.trustedRelayList.flow.value + .toList() - override fun saveRelayList(urlList: List) { + override suspend fun saveRelayList(urlList: List) { account.saveTrustedRelayList(urlList) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt index a8ea3ea388..b82d461cd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddInboxRelayForSearchCard.kt @@ -37,11 +37,10 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -50,8 +49,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.imageModifier -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent @Preview @Composable @@ -69,46 +66,14 @@ fun ObserveRelayListForSearchAndDisplayIfNotFound( accountViewModel: AccountViewModel, nav: INav, ) { - ObserveRelayListForSearch( - accountViewModel = accountViewModel, - ) { relayListEvent -> - if (relayListEvent == null || relayListEvent.relays().isEmpty()) { - AddInboxRelayForSearchCard( - accountViewModel = accountViewModel, - nav = nav, - ) - } - } -} + val searchRelayList by accountViewModel.account.searchRelayList.flow + .collectAsStateWithLifecycle() -@Composable -fun ObserveRelayListForSearch( - accountViewModel: AccountViewModel, - inner: @Composable (relayListEvent: SearchRelayListEvent?) -> Unit, -) { - ObserveRelayListForSearch( - pubkey = accountViewModel.account.userProfile().pubkeyHex, - accountViewModel = accountViewModel, - ) { relayListEvent -> - inner(relayListEvent) - } -} - -@Composable -fun ObserveRelayListForSearch( - pubkey: HexKey, - accountViewModel: AccountViewModel, - inner: @Composable (relayListEvent: SearchRelayListEvent?) -> Unit, -) { - LoadAddressableNote( - SearchRelayListEvent.createAddress(pubkey), - accountViewModel, - ) { relayList -> - if (relayList != null) { - val relayListEvent by observeNoteEvent(relayList, accountViewModel) - - inner(relayListEvent) - } + if (searchRelayList.isEmpty()) { + AddInboxRelayForSearchCard( + accountViewModel = accountViewModel, + nav = nav, + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt index 5735340253..651223d18a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/AddSearchRelayListDialog.kt @@ -59,9 +59,9 @@ fun AddSearchRelayListDialog( ) { val postViewModel: SearchRelayListViewModel = viewModel() - postViewModel.init(accountViewModel.account) + postViewModel.init(accountViewModel) - LaunchedEffect(accountViewModel.account) { + LaunchedEffect(postViewModel, accountViewModel.account) { postViewModel.load() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt index 116fc3e298..6507271c18 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/NIP47SetupScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R @@ -41,7 +42,12 @@ fun NIP47SetupScreen( nip47: String?, ) { val postViewModel: UpdateZapAmountViewModel = viewModel() - postViewModel.load(accountViewModel.account) + postViewModel.init(accountViewModel) + + LaunchedEffect(accountViewModel, postViewModel) { + postViewModel.load() + } + NIP47SetupScreen(postViewModel, accountViewModel, nav, nip47) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 1a3f2f70eb..02b5a801c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -264,7 +264,7 @@ private fun HeaderOptions(accountViewModel: AccountViewModel) { placeholder = selectedItens[parseWarningType(sensitive).screenCode].title, options = selectedItens, onSelect = { - accountViewModel.updateContentWarnings(parseWarningType(it)) + accountViewModel.updateShowSensitiveContent(parseWarningType(it).prefCode) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt index b8603ca60c..60d35bcfc0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -46,7 +46,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav @@ -58,8 +57,6 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import java.util.Locale as JavaLocale @Preview(device = "spec:width=2160px,height=2340px,dpi=440") @@ -129,11 +126,7 @@ fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { DropdownMenuItem( text = { Text(text = JavaLocale.forLanguageTag(languageCode).displayName) }, onClick = { - accountViewModel.viewModelScope.launch(Dispatchers.IO) { - accountViewModel.toggleDontTranslateFrom( - languageCode, - ) - } + accountViewModel.toggleDontTranslateFrom(languageCode) selectedLanguages.remove(languageCode) expanded = false }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 52e91ab634..c04fd1310c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -204,12 +204,12 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent -import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt index 9609d9a98d..0d6e5aafe3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/VideoScreen.kt @@ -421,7 +421,7 @@ fun ReactionsColumn( ) { routeFor( baseNote, - accountViewModel.userProfile(), + accountViewModel.account, )?.let { nav.nav(it) } } BoostReaction( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt index 3e669f0dcc..c95b286abb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt @@ -31,8 +31,8 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.SUPPORTED_VIDEO_FEED_MIME_TYPES_SET import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoMeta diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt index 309065eadd..0cc8ba4273 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt @@ -53,7 +53,7 @@ fun ExternalSignerButton(loginViewModel: LoginViewModel) { val resultData = result.data if (result.resultCode == Activity.RESULT_OK && resultData != null) { val loginInfo = ExternalSignerLogin.parseResult(resultData) - if (loginInfo is SignerResult.Successful) { + if (loginInfo is SignerResult.RequestAddressed.Successful) { loginViewModel.updateKey(TextFieldValue(loginInfo.result.pubkey), false) loginViewModel.loginWithExternalSigner(loginInfo.result.packageName) } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 0ab41e9cf7..141ca6b28c 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -55,6 +55,13 @@ You are using a public key and public keys are read-only. Login with a Private key to be able to unfollow You are using a public key and public keys are read-only. Login with a Private key to be able to hide a word or sentence You are using a public key and public keys are read-only. Login with a Private key to be able to show a word or sentence + You are using a public key and public keys are read-only. Login with a Private key to be able to change settings + You are using a public key and public keys are read-only. Login with a Private key to be able to upload + + You are using a public key and public keys are read-only. Login with a Private key to be able to sign for events + + Unauthorized Decryption + The signer did not authorize a decryption that is required to make this operation. Activate NIP-44 decryptions in your signer app and try again Zaps diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index 8d43e8b211..03b7d5f790 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -244,10 +244,8 @@ private fun TranslationMessage( } }, onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.account.prefer(source, target, source) - langSettingsPopupExpanded = false - } + accountViewModel.prefer(source, target, source) + langSettingsPopupExpanded = false }, ) DropdownMenuItem( @@ -277,7 +275,7 @@ private fun TranslationMessage( }, onClick = { scope.launch(Dispatchers.IO) { - accountViewModel.account.prefer(source, target, target) + accountViewModel.prefer(source, target, target) langSettingsPopupExpanded = false } }, @@ -311,10 +309,8 @@ private fun TranslationMessage( } }, onClick = { - scope.launch(Dispatchers.IO) { - accountViewModel.account.updateTranslateTo(lang) - langSettingsPopupExpanded = false - } + langSettingsPopupExpanded = false + accountViewModel.updateTranslateTo(lang) }, ) } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt index 607cf541f8..cad8498c35 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapBenchmark.kt @@ -32,14 +32,11 @@ import com.vitorpamplona.quartz.nip17Dm.NIP17Factory import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import junit.framework.TestCase +import kotlinx.coroutines.runBlocking import org.junit.Assert -import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit /** * Benchmark, which will execute on an Android device. @@ -54,102 +51,78 @@ class GiftWrapBenchmark { fun basePerformanceTest( message: String, expectedLength: Int, - ) { + ) = runBlocking { val sender = NostrSignerInternal(KeyPair()) val receiver = NostrSignerInternal(KeyPair()) - var events: NIP17Factory.Result? = null - val countDownLatch = CountDownLatch(1) - - NIP17Factory().createMessageNIP17( - ChatMessageEvent.build( - message, - listOf(PTag(receiver.pubKey)), - ), - sender, - ) { - events = it - countDownLatch.countDown() - } - - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - - val countDownLatch2 = CountDownLatch(1) + val result = + NIP17Factory().createMessageNIP17( + ChatMessageEvent.build( + message, + listOf(PTag(receiver.pubKey)), + ), + sender, + ) Assert.assertEquals( expectedLength, - events!! - .wraps - .sumOf { it.toJson().length }, + result.wraps.sumOf { it.toJson().length }, ) // Simulate Receiver - events!!.wraps.forEach { + result.wraps.forEach { it.checkSignature() val keyToUse = if (it.recipientPubKey() == sender.pubKey) sender else receiver - it.unwrap(keyToUse) { event -> - event.checkSignature() + val event = it.unwrapThrowing(keyToUse) + event.checkSignature() - if (event is SealedRumorEvent) { - event.unseal(keyToUse) { innerData -> - Assert.assertEquals(message, innerData.content) - countDownLatch2.countDown() - } - } else { - Assert.fail("Wrong Event") - } + if (event is SealedRumorEvent) { + val innerData = event.unsealThrowing(keyToUse) + Assert.assertEquals(message, innerData.content) + } else { + Assert.fail("Wrong Event") } } - - assertTrue(countDownLatch2.await(1, TimeUnit.SECONDS)) } fun receivePerformanceTest(message: String) { val sender = NostrSignerInternal(KeyPair()) val receiver = NostrSignerInternal(KeyPair()) - var giftWrap: GiftWrapEvent? = null - val countDownLatch = CountDownLatch(1) + val result = + runBlocking { + NIP17Factory().createMessageNIP17( + ChatMessageEvent.build( + message, + listOf(PTag(receiver.pubKey)), + ), + sender, + ) + } - NIP17Factory().createMessageNIP17( - ChatMessageEvent.build( - message, - listOf(PTag(receiver.pubKey)), - ), - sender, - ) { - giftWrap = it.wraps.first() - countDownLatch.countDown() - } + val giftWrap = result.wraps.first() - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - - val keyToUse = if (giftWrap!!.recipientPubKey() == sender.pubKey) sender else receiver - val giftWrapJson = giftWrap!!.toJson() + val keyToUse = if (giftWrap.recipientPubKey() == sender.pubKey) sender else receiver + val giftWrapJson = giftWrap.toJson() // Simulate Receiver benchmarkRule.measureRepeated { - val counter = CountDownLatch(1) + runBlocking { + val wrap = Event.fromJson(giftWrapJson) as GiftWrapEvent + wrap.checkSignature() - val wrap = Event.fromJson(giftWrapJson) as GiftWrapEvent - wrap.checkSignature() - - wrap.unwrap(keyToUse) { seal -> + val seal = wrap.unwrapThrowing(keyToUse) seal.checkSignature() if (seal is SealedRumorEvent) { - seal.unseal(keyToUse) { innerData -> - Assert.assertEquals(message, innerData.content) - counter.countDown() - } + val innerData = seal.unsealThrowing(keyToUse) + Assert.assertEquals(message, innerData.content) } else { Assert.fail("Wrong Event") } } - - TestCase.assertTrue(counter.await(1, TimeUnit.SECONDS)) } } diff --git a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt index a7e14efd6b..678a654fbc 100644 --- a/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt +++ b/commons/src/androidTest/java/com/vitorpamplona/amethyst/commons/preview/BlurhashTest.kt @@ -96,7 +96,7 @@ class BlurhashTest { @Test fun testLorikeet() { val blurhash = load("/lorikeet.jpg").toBlurhash() - assertEquals("rFDcT@_LNs#p%Mt*nNM}E2VrIVX6VuV@WUo{xtjv9]RRw[OXS}rrWFX9w{OZxaxWNHX4n\$M}NGaK%0RkM}w{xto|jFs,Sh-Tj]bcwJnjXSxZs.NI", blurhash) + assertEquals("rFDcT@_LNs#:-pyBnhRRE2Z~MyX5VuV@WUo{xta\$9]RQw[OXS}rrWFXSw|OsxaxWNHSwn~M}NGaK%0RkM}w{xto|jGs+Sh-Tj]W?wJnjXSxGs.NI", blurhash) } private fun load(filename: String): Bitmap = diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/AsyncCachedState.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/AsyncCachedState.kt index 088fd830dd..733dfdac20 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/AsyncCachedState.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/compose/AsyncCachedState.kt @@ -32,8 +32,9 @@ fun produceCachedStateAsync( ): State = @Suppress("ProduceStateDoesNotAssignValue") produceState(initialValue = cache.cached(key), key1 = key) { - cache.update(key) { - value = it + val newValue = cache.update(key) + if (newValue != value) { + value = newValue } } @@ -45,18 +46,16 @@ fun produceCachedStateAsync( ): State = @Suppress("ProduceStateDoesNotAssignValue") produceState(initialValue = cache.cached(updateValue), key1 = key) { - cache.update(updateValue) { - value = it + val newValue = cache.update(updateValue) + if (newValue != value) { + value = newValue } } interface AsyncCachedState { fun cached(k: K): V? - suspend fun update( - k: K, - onReady: (V?) -> Unit, - ) + suspend fun update(k: K): V? } abstract class GenericBaseCacheAsync( @@ -66,23 +65,17 @@ abstract class GenericBaseCacheAsync( override fun cached(k: K): V? = cache[k] - override suspend fun update( - k: K, - onReady: (V?) -> Unit, - ) { - cache[k]?.let { onReady(it) } + override suspend fun update(k: K): V? { + cache[k]?.let { return it } - compute(k) { - if (it != null) { - cache.put(k, it) - } + val newValue = compute(k) - onReady(it) + if (newValue != null) { + cache.put(k, newValue) } + + return newValue } - abstract suspend fun compute( - key: K, - onReady: (V?) -> Unit, - ) + abstract suspend fun compute(key: K): V? } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt index be92b41be2..0cbd18b877 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip03Timestamp/ots/OtsTest.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.fail +import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Test import java.util.concurrent.CountDownLatch @@ -74,17 +75,13 @@ class OtsTest { val signer = NostrSignerInternal(KeyPair()) - var newOts: OtsEvent? = null val countDownLatch = CountDownLatch(1) - signer.sign(OtsEvent.build(eventId, upgraded!!)) { - newOts = it - countDownLatch.countDown() - } + val newOts = runBlocking { signer.sign(OtsEvent.build(eventId, upgraded!!)) } Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - println(newOts!!.toJson()) + println(newOts.toJson()) println(resolver.info(newOts.otsByteArray())) assertEquals(1708879025L, newOts.verify(resolver)) @@ -93,18 +90,17 @@ class OtsTest { @Test fun createOTSEventAndVerify() { val signer = NostrSignerInternal(KeyPair()) - var ots: OtsEvent? = null val countDownLatch = CountDownLatch(1) - signer.sign(OtsEvent.build(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest, resolver))) { - ots = it - countDownLatch.countDown() - } + val ots = + runBlocking { + signer.sign(OtsEvent.build(otsEvent2Digest, OtsEvent.stamp(otsEvent2Digest, resolver))) + } Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - println(ots!!.toJson()) + println(ots.toJson()) println(resolver.info(ots.otsByteArray())) assertEquals(null, ots.verify(resolver)) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt index 286a6ce0d8..9de3c4188f 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt @@ -27,10 +27,10 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip02FollowList.ReadWrite import com.vitorpamplona.quartz.utils.TimeUtils import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.runBlocking import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith -import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) internal class Nip46Test { @@ -49,206 +49,205 @@ internal class Nip46Test { sig = "", ) - fun encodeDecodeEvent(req: T): T { - var countDownLatch = CountDownLatch(1) - var eventStr: String? = null + suspend fun encodeDecodeEvent(req: T): T { + val eventStr = NostrConnectEvent.create(req, remoteKey.pubKey, signer).toJson() - NostrConnectEvent.create(req, remoteKey.pubKey, signer) { - eventStr = it.toJson() - countDownLatch.countDown() + return (Event.fromJson(eventStr) as NostrConnectEvent).decryptMessage(signer) as T + } + + @Test + fun signEncoder() = + runBlocking { + val expected = BunkerRequestSign(event = dummyEvent) + val actual = encodeDecodeEvent(expected) + + assertEquals(BunkerRequestSign.METHOD_NAME, actual.method) + assertEquals(expected.id, actual.id) + assertEquals(dummyEvent.id, actual.event.id) } - countDownLatch.await() + @Test + fun connectEncoder() = + runBlocking { + val expected = BunkerRequestConnect(remoteKey = remoteKey.pubKey) + val actual = encodeDecodeEvent(expected) - countDownLatch = CountDownLatch(1) - var innerMessage: T? = null - - (Event.fromJson(eventStr!!) as NostrConnectEvent).plainContent(signer) { - innerMessage = it as T - countDownLatch.countDown() + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) } - countDownLatch.await() + @Test + fun pingEncoder() = + runBlocking { + val expected = BunkerRequestPing() + val actual = encodeDecodeEvent(expected) - return innerMessage!! - } + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + } @Test - fun signEncoder() { - val expected = BunkerRequestSign(event = dummyEvent) - val actual = encodeDecodeEvent(expected) + fun getPubkeyEncoder() = + runBlocking { + val expected = BunkerRequestGetPublicKey() + val actual = encodeDecodeEvent(expected) - assertEquals(BunkerRequestSign.METHOD_NAME, actual.method) - assertEquals(expected.id, actual.id) - assertEquals(dummyEvent.id, actual.event.id) - } + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + } @Test - fun connectEncoder() { - val expected = BunkerRequestConnect(remoteKey = remoteKey.pubKey) - val actual = encodeDecodeEvent(expected) + fun getRelaysEncoder() = + runBlocking { + val expected = BunkerRequestGetRelays() + val actual = encodeDecodeEvent(expected) - assertEquals(expected.method, actual.method) - assertEquals(expected.id, actual.id) - } + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + } @Test - fun pingEncoder() { - val expected = BunkerRequestPing() - val actual = encodeDecodeEvent(expected) + fun testNip04Encrypt() = + runBlocking { + val expected = BunkerRequestNip04Encrypt(pubKey = peer.pubKey, message = "Test") + val actual = encodeDecodeEvent(expected) - assertEquals(expected.method, actual.method) - assertEquals(expected.id, actual.id) - } + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + assertEquals(expected.pubKey, actual.pubKey) + assertEquals(expected.message, actual.message) + } @Test - fun getPubkeyEncoder() { - val expected = BunkerRequestGetPublicKey() - val actual = encodeDecodeEvent(expected) + fun testNip44Encrypt() = + runBlocking { + val expected = BunkerRequestNip44Encrypt(pubKey = peer.pubKey, message = "Test") + val actual = encodeDecodeEvent(expected) - assertEquals(expected.method, actual.method) - assertEquals(expected.id, actual.id) - } + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + assertEquals(expected.pubKey, actual.pubKey) + assertEquals(expected.message, actual.message) + } @Test - fun getRelaysEncoder() { - val expected = BunkerRequestGetRelays() - val actual = encodeDecodeEvent(expected) + fun testNip04Decrypt() = + runBlocking { + val expected = BunkerRequestNip04Decrypt(pubKey = peer.pubKey, ciphertext = "Test") + val actual = encodeDecodeEvent(expected) - assertEquals(expected.method, actual.method) - assertEquals(expected.id, actual.id) - } + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + assertEquals(expected.pubKey, actual.pubKey) + assertEquals(expected.ciphertext, actual.ciphertext) + } @Test - fun testNip04Encrypt() { - val expected = BunkerRequestNip04Encrypt(pubKey = peer.pubKey, message = "Test") - val actual = encodeDecodeEvent(expected) + fun testNip44Decrypt() = + runBlocking { + val expected = BunkerRequestNip44Decrypt(pubKey = peer.pubKey, ciphertext = "Test") + val actual = encodeDecodeEvent(expected) - assertEquals(expected.method, actual.method) - assertEquals(expected.id, actual.id) - assertEquals(expected.pubKey, actual.pubKey) - assertEquals(expected.message, actual.message) - } - - @Test - fun testNip44Encrypt() { - val expected = BunkerRequestNip44Encrypt(pubKey = peer.pubKey, message = "Test") - val actual = encodeDecodeEvent(expected) - - assertEquals(expected.method, actual.method) - assertEquals(expected.id, actual.id) - assertEquals(expected.pubKey, actual.pubKey) - assertEquals(expected.message, actual.message) - } - - @Test - fun testNip04Decrypt() { - val expected = BunkerRequestNip04Decrypt(pubKey = peer.pubKey, ciphertext = "Test") - val actual = encodeDecodeEvent(expected) - - assertEquals(expected.method, actual.method) - assertEquals(expected.id, actual.id) - assertEquals(expected.pubKey, actual.pubKey) - assertEquals(expected.ciphertext, actual.ciphertext) - } - - @Test - fun testNip44Decrypt() { - val expected = BunkerRequestNip44Decrypt(pubKey = peer.pubKey, ciphertext = "Test") - val actual = encodeDecodeEvent(expected) - - assertEquals(expected.method, actual.method) - assertEquals(expected.id, actual.id) - assertEquals(expected.pubKey, actual.pubKey) - assertEquals(expected.ciphertext, actual.ciphertext) - } + assertEquals(expected.method, actual.method) + assertEquals(expected.id, actual.id) + assertEquals(expected.pubKey, actual.pubKey) + assertEquals(expected.ciphertext, actual.ciphertext) + } // Responses @Test - fun testAckResponse() { - val expected = BunkerResponseAck() - val actual = encodeDecodeEvent(expected) + fun testAckResponse() = + runBlocking { + val expected = BunkerResponseAck() + val actual = encodeDecodeEvent(expected) - assertEquals(expected.id, actual.id) - assertEquals(expected.result, actual.result) - assertEquals(expected.error, actual.error) - } + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + } @Test - fun testPongResponse() { - val expected = BunkerResponsePong() - val actual = encodeDecodeEvent(expected) + fun testPongResponse() = + runBlocking { + val expected = BunkerResponsePong() + val actual = encodeDecodeEvent(expected) - assertEquals(expected.id, actual.id) - assertEquals(expected.result, actual.result) - assertEquals(expected.error, actual.error) - } + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + } @Test - fun testErrorResponse() { - val expected = BunkerResponseError(error = "Error") - val actual = encodeDecodeEvent(expected) + fun testErrorResponse() = + runBlocking { + val expected = BunkerResponseError(error = "Error") + val actual = encodeDecodeEvent(expected) - assertEquals(expected.id, actual.id) - assertEquals(expected.result, actual.result) - assertEquals(expected.error, actual.error) - } + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + } @Test - fun testEventResponse() { - val expected = BunkerResponseEvent(event = dummyEvent) - val actual = encodeDecodeEvent(expected) + fun testEventResponse() = + runBlocking { + val expected = BunkerResponseEvent(event = dummyEvent) + val actual = encodeDecodeEvent(expected) - assertEquals(expected.id, actual.id) - assertEquals(expected.result, actual.result) - assertEquals(expected.error, actual.error) - assertEquals(dummyEvent.id, actual.event.id) - } + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + assertEquals(dummyEvent.id, actual.event.id) + } @Test - fun testPubkeyResponse() { - val expected = BunkerResponsePublicKey(pubkey = peer.pubKey) - val actual = encodeDecodeEvent(expected) + fun testPubkeyResponse() = + runBlocking { + val expected = BunkerResponsePublicKey(pubkey = peer.pubKey) + val actual = encodeDecodeEvent(expected) - assertEquals(expected.id, actual.id) - assertEquals(expected.result, actual.result) - assertEquals(expected.error, actual.error) - assertEquals(expected.pubkey, actual.pubkey) - } + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + assertEquals(expected.pubkey, actual.pubkey) + } @Test - fun testRelaysResponse() { - val expected = BunkerResponseGetRelays(relays = mapOf("url" to ReadWrite(true, false))) - val actual = encodeDecodeEvent(expected) + fun testRelaysResponse() = + runBlocking { + val expected = BunkerResponseGetRelays(relays = mapOf("url" to ReadWrite(true, false))) + val actual = encodeDecodeEvent(expected) - assertEquals(expected.id, actual.id) - assertEquals(expected.result, actual.result) - assertEquals(expected.error, actual.error) - assertEquals(expected.relays["url"], actual.relays["url"]) - } + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + assertEquals(expected.relays["url"], actual.relays["url"]) + } @Test @Ignore("Impossible to recreate the class since there are no hints on the json") - fun testDecryptResponse() { - val expected = BunkerResponseDecrypt(plaintext = "Test") - val actual = encodeDecodeEvent(expected) + fun testDecryptResponse() = + runBlocking { + val expected = BunkerResponseDecrypt(plaintext = "Test") + val actual = encodeDecodeEvent(expected) - assertEquals(expected.id, actual.id) - assertEquals(expected.result, actual.result) - assertEquals(expected.error, actual.error) - assertEquals(expected.plaintext, actual.plaintext) - } + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + assertEquals(expected.plaintext, actual.plaintext) + } @Test @Ignore("Impossible to recreate the class since there are no hints on the json") - fun testEncryptResponse() { - val expected = BunkerResponseEncrypt(ciphertext = "Test") - val actual = encodeDecodeEvent(expected) + fun testEncryptResponse() = + runBlocking { + val expected = BunkerResponseEncrypt(ciphertext = "Test") + val actual = encodeDecodeEvent(expected) - assertEquals(expected.id, actual.id) - assertEquals(expected.result, actual.result) - assertEquals(expected.error, actual.error) - assertEquals(expected.ciphertext, actual.ciphertext) - } + assertEquals(expected.id, actual.id) + assertEquals(expected.result, actual.result) + assertEquals(expected.error, actual.error) + assertEquals(expected.ciphertext, actual.ciphertext) + } } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt index ca39a13545..cd3056d962 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt @@ -27,10 +27,10 @@ import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEncryption.Companion.createEncryptionPrivateKey -import com.vitorpamplona.quartz.nip59GiftWraps.wait1SecondForResult import com.vitorpamplona.quartz.utils.Hex import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.fail +import kotlinx.coroutines.runBlocking import org.junit.Test import org.junit.runner.RunWith @@ -89,42 +89,35 @@ class PrivateZapTests { ), ) - var resultPrivateZap: Event? = null - - wait1SecondForResult { onDone -> - LnZapRequestEvent.create( - originalNote = poll, - relays = setOf("wss://relay.damus.io/"), - signer = loggedIn, - pollOption = 0, - message = "", - zapType = LnZapEvent.ZapType.PRIVATE, - toUserPubHex = null, - ) { privateZapRequest -> - val recepientPK = privateZapRequest.zappedAuthor().firstOrNull() - val recepientPost = privateZapRequest.zappedPost().firstOrNull() - - if (recepientPK != null && recepientPost != null) { - val privateKey = - createEncryptionPrivateKey( - loggedIn.keyPair.privKey!!.toHexKey(), - recepientPost, - privateZapRequest.createdAt, - ) - val decodedPrivateZap = privateZapRequest.getPrivateZapEvent(privateKey, recepientPK) - - println(decodedPrivateZap?.toJson()) - - resultPrivateZap = decodedPrivateZap - - onDone() - } else { - fail("Should not be null") - } + val privateZapRequest = + runBlocking { + LnZapRequestEvent.create( + originalNote = poll, + relays = setOf("wss://relay.damus.io/"), + signer = loggedIn, + pollOption = 0, + message = "", + zapType = LnZapEvent.ZapType.PRIVATE, + toUserPubHex = null, + ) } - } - assertNotNull(resultPrivateZap) + val recepientPK = privateZapRequest.zappedAuthor().firstOrNull() + val recepientPost = privateZapRequest.zappedPost().firstOrNull() + + if (recepientPK != null && recepientPost != null) { + val privateKey = + createEncryptionPrivateKey( + loggedIn.keyPair.privKey!!.toHexKey(), + recepientPost, + privateZapRequest.createdAt, + ) + val decodedPrivateZap = PrivateZapRequestBuilder().decryptAnonTag(privateZapRequest.getAnonTag(), privateKey, recepientPK) + + assertNotNull(decodedPrivateZap) + } else { + fail("Should not be null") + } } @Test @@ -156,41 +149,34 @@ class PrivateZapTests { KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), ) - var resultPrivateZap: Event? = null - - wait1SecondForResult { onDone -> - LnZapRequestEvent.create( - originalNote = textNote, - relays = setOf("wss://relay.damus.io/", "wss://relay.damus2.io/", "wss://relay.damus3.io/"), - signer = loggedIn, - pollOption = null, - message = "test", - zapType = LnZapEvent.ZapType.PRIVATE, - toUserPubHex = null, - ) { privateZapRequest -> - val recepientPK = privateZapRequest.zappedAuthor().firstOrNull() - val recepientPost = privateZapRequest.zappedPost().firstOrNull() - - if (recepientPK != null && recepientPost != null) { - val privateKey = - createEncryptionPrivateKey( - loggedIn.keyPair.privKey!!.toHexKey(), - recepientPost, - privateZapRequest.createdAt, - ) - val decodedPrivateZap = privateZapRequest.getPrivateZapEvent(privateKey, recepientPK) - - println(decodedPrivateZap?.toJson()) - - resultPrivateZap = decodedPrivateZap - - onDone() - } else { - fail("Should not be null") - } + val privateZapRequest = + runBlocking { + LnZapRequestEvent.create( + originalNote = textNote, + relays = setOf("wss://relay.damus.io/", "wss://relay.damus2.io/", "wss://relay.damus3.io/"), + signer = loggedIn, + pollOption = null, + message = "test", + zapType = LnZapEvent.ZapType.PRIVATE, + toUserPubHex = null, + ) } - } - assertNotNull(resultPrivateZap) + val recepientPK = privateZapRequest.zappedAuthor().firstOrNull() + val recepientPost = privateZapRequest.zappedPost().firstOrNull() + + if (recepientPK != null && recepientPost != null) { + val privateKey = + createEncryptionPrivateKey( + loggedIn.keyPair.privKey!!.toHexKey(), + recepientPost, + privateZapRequest.createdAt, + ) + val decodedPrivateZap = PrivateZapRequestBuilder().decryptAnonTag(privateZapRequest.getAnonTag(), privateKey, recepientPK) + + assertNotNull(decodedPrivateZap) + } else { + fail("Should not be null") + } } } diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt index d37391915a..1b244a2b30 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip59GiftWraps/GiftWrapEventTest.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.utils.Hex +import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotNull @@ -41,104 +42,87 @@ import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Test import org.junit.runner.RunWith -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit @RunWith(AndroidJUnit4::class) class GiftWrapEventTest { @Test() - fun testNip17Utils() { - val sender = NostrSignerInternal(KeyPair()) - val receiver = NostrSignerInternal(KeyPair()) - val message = "Hola, que tal?" + fun testNip17Utils() = + runBlocking { + val sender = NostrSignerInternal(KeyPair()) + val receiver = NostrSignerInternal(KeyPair()) + val message = "Hola, que tal?" - // Requires 3 tests - val countDownLatch = CountDownLatch(3) - - NIP17Factory().createMessageNIP17( - ChatMessageEvent.build( - message, - listOf(PTag(receiver.pubKey, null)), - ), - sender, - ) { events -> - countDownLatch.countDown() + val events = + NIP17Factory().createMessageNIP17( + ChatMessageEvent.build( + message, + listOf(PTag(receiver.pubKey, null)), + ), + sender, + ) // Simulate Receiver val eventsReceiverGets = events.wraps.filter { it.isTaggedUser(receiver.pubKey) } eventsReceiverGets.forEach { - it.unwrap(receiver) { event -> - if (event is SealedRumorEvent) { - event.unseal(receiver) { innerData -> - countDownLatch.countDown() - assertEquals(message, innerData.content) - } - } else { - fail("Wrong Event") - } + val event = it.unwrapThrowing(receiver) + if (event is SealedRumorEvent) { + val innerData = event.unsealThrowing(receiver) + assertEquals(message, innerData.content) + } else { + fail("Wrong Event") } } // Simulate Sender val eventsSenderGets = events.wraps.filter { it.isTaggedUser(sender.pubKey) } eventsSenderGets.forEach { - it.unwrap(sender) { event -> - if (event is SealedRumorEvent) { - event.unseal(sender) { innerData -> - countDownLatch.countDown() - assertEquals(message, innerData.content) - } - } else { - fail("Wrong Event") - } + val event = it.unwrapThrowing(sender) + if (event is SealedRumorEvent) { + val innerData = event.unsealThrowing(sender) + assertEquals(message, innerData.content) + } else { + fail("Wrong Event") } } } - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - } - @Test() - fun testNip17UtilsForGroups() { - val sender = NostrSignerInternal(KeyPair()) - val receiver1 = NostrSignerInternal(KeyPair()) - val receiver2 = NostrSignerInternal(KeyPair()) - val receiver3 = NostrSignerInternal(KeyPair()) - val receiver4 = NostrSignerInternal(KeyPair()) - val message = "Hola, que tal?" + fun testNip17UtilsForGroups() = + runBlocking { + val sender = NostrSignerInternal(KeyPair()) + val receiver1 = NostrSignerInternal(KeyPair()) + val receiver2 = NostrSignerInternal(KeyPair()) + val receiver3 = NostrSignerInternal(KeyPair()) + val receiver4 = NostrSignerInternal(KeyPair()) + val message = "Hola, que tal?" - val receivers = - listOf( - receiver1, - receiver2, - receiver3, - receiver4, - ) + val receivers = + listOf( + receiver1, + receiver2, + receiver3, + receiver4, + ) - val countDownLatch = CountDownLatch(receivers.size + 2) - - NIP17Factory().createMessageNIP17( - ChatMessageEvent.build( - message, - receivers.map { PTag(it.pubKey, null) }, - ), - sender, - ) { events -> - countDownLatch.countDown() + val events = + NIP17Factory().createMessageNIP17( + ChatMessageEvent.build( + message, + receivers.map { PTag(it.pubKey, null) }, + ), + sender, + ) // Simulate Receiver receivers.forEach { receiver -> val eventsReceiverGets = events.wraps.filter { it.isTaggedUser(receiver.pubKey) } eventsReceiverGets.forEach { - it.unwrap(receiver) { event -> - if (event is SealedRumorEvent) { - event.unseal(receiver) { innerData -> - countDownLatch.countDown() - assertEquals(message, innerData.content) - } - } else { - fail("Wrong Event") - } + val event = it.unwrapThrowing(receiver) + if (event is SealedRumorEvent) { + val innerData = event.unsealThrowing(receiver) + assertEquals(message, innerData.content) + } else { + fail("Wrong Event") } } } @@ -146,149 +130,126 @@ class GiftWrapEventTest { // Simulate Sender val eventsSenderGets = events.wraps.filter { it.isTaggedUser(sender.pubKey) } eventsSenderGets.forEach { - it.unwrap(sender) { event -> - if (event is SealedRumorEvent) { - event.unseal(sender) { innerData -> - countDownLatch.countDown() - assertEquals(message, innerData.content) - } - } else { - fail("Wrong Event") - } + val event = it.unwrapThrowing(sender) + if (event is SealedRumorEvent) { + val innerData = event.unsealThrowing(sender) + assertEquals(message, innerData.content) + } else { + fail("Wrong Event") } } } - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - } - @Test() - fun testInternalsSimpleMessage() { - val sender = NostrSignerInternal(KeyPair()) - val receiver = NostrSignerInternal(KeyPair()) + fun testInternalsSimpleMessage() = + runBlocking { + val sender = NostrSignerInternal(KeyPair()) + val receiver = NostrSignerInternal(KeyPair()) - val countDownLatch = CountDownLatch(2) - - var giftWrapEventToSender: GiftWrapEvent? = null - var giftWrapEventToReceiver: GiftWrapEvent? = null - - sender.sign( - ChatMessageEvent.build( - msg = "Hi There!", - to = listOf(PTag(receiver.pubKey, null)), - ), - ) { senderMessage -> + val senderMessage = + sender.sign( + ChatMessageEvent.build( + msg = "Hi There!", + to = listOf(PTag(receiver.pubKey, null)), + ), + ) // MsgFor the Receiver + val encMsgFromSenderToReceiver = + SealedRumorEvent.create( + event = senderMessage, + encryptTo = receiver.pubKey, + signer = sender, + ) - SealedRumorEvent.create( - event = senderMessage, - encryptTo = receiver.pubKey, - signer = sender, - ) { encMsgFromSenderToReceiver -> - // Should expose sender - assertEquals(encMsgFromSenderToReceiver.pubKey, sender.pubKey) - // Should not expose receiver - assertTrue(encMsgFromSenderToReceiver.tags.isEmpty()) + // Should expose sender + assertEquals(encMsgFromSenderToReceiver.pubKey, sender.pubKey) + // Should not expose receiver + assertTrue(encMsgFromSenderToReceiver.tags.isEmpty()) + val giftWrapToReceiver = GiftWrapEvent.create( event = encMsgFromSenderToReceiver, recipientPubKey = receiver.pubKey, - ) { giftWrapToReceiver -> - // Should not be signed by neither sender nor receiver - assertNotEquals(giftWrapToReceiver.pubKey, sender.pubKey) - assertNotEquals(giftWrapToReceiver.pubKey, receiver.pubKey) + ) - // Should not include sender as recipient - assertNotEquals(giftWrapToReceiver.recipientPubKey(), sender.pubKey) + // Should not be signed by neither sender nor receiver + assertNotEquals(giftWrapToReceiver.pubKey, sender.pubKey) + assertNotEquals(giftWrapToReceiver.pubKey, receiver.pubKey) - // Should be addressed to the receiver - assertEquals(giftWrapToReceiver.recipientPubKey(), receiver.pubKey) + // Should not include sender as recipient + assertNotEquals(giftWrapToReceiver.recipientPubKey(), sender.pubKey) - giftWrapEventToReceiver = giftWrapToReceiver - - countDownLatch.countDown() - } - } + // Should be addressed to the receiver + assertEquals(giftWrapToReceiver.recipientPubKey(), receiver.pubKey) // MsgFor the Sender - SealedRumorEvent.create( - event = senderMessage, - encryptTo = sender.pubKey, - signer = sender, - ) { encMsgFromSenderToSender -> - // Should expose sender - assertEquals(encMsgFromSenderToSender.pubKey, sender.pubKey) - // Should not expose receiver - assertTrue(encMsgFromSenderToSender.tags.isEmpty()) + val encMsgFromSenderToSender = + SealedRumorEvent.create( + event = senderMessage, + encryptTo = sender.pubKey, + signer = sender, + ) + // Should expose sender + assertEquals(encMsgFromSenderToSender.pubKey, sender.pubKey) + // Should not expose receiver + assertTrue(encMsgFromSenderToSender.tags.isEmpty()) + + val giftWrapToSender = GiftWrapEvent.create( event = encMsgFromSenderToSender, recipientPubKey = sender.pubKey, - ) { giftWrapToSender -> - // Should not be signed by neither the sender, not the receiver - assertNotEquals(giftWrapToSender.pubKey, sender.pubKey) - assertNotEquals(giftWrapToSender.pubKey, receiver.pubKey) + ) - // Should not be addressed to the receiver - assertNotEquals(giftWrapToSender.recipientPubKey(), receiver.pubKey) - // Should be addressed to the sender - assertEquals(giftWrapToSender.recipientPubKey(), sender.pubKey) + // Should not be signed by neither the sender, not the receiver + assertNotEquals(giftWrapToSender.pubKey, sender.pubKey) + assertNotEquals(giftWrapToSender.pubKey, receiver.pubKey) - giftWrapEventToSender = giftWrapToSender + // Should not be addressed to the receiver + assertNotEquals(giftWrapToSender.recipientPubKey(), receiver.pubKey) + // Should be addressed to the sender + assertEquals(giftWrapToSender.recipientPubKey(), sender.pubKey) - countDownLatch.countDown() - } - } - } + // Done + // ----- + // Start receiving - // Done - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + // Receiver's side + // Makes sure it can only be decrypted by the target user - // Receiver's side - // Makes sure it can only be decrypted by the target user + assertNotNull(giftWrapToSender) + assertNotNull(giftWrapToReceiver) - assertNotNull(giftWrapEventToSender) - assertNotNull(giftWrapEventToReceiver) - - val countDownDecryptLatch = CountDownLatch(2) - - giftWrapEventToSender!!.unwrap(sender) { unwrappedMsgForSenderBySender -> + val unwrappedMsgForSenderBySender = giftWrapToSender.unwrapThrowing(sender) assertEquals(SealedRumorEvent.KIND, unwrappedMsgForSenderBySender.kind) assertTrue(unwrappedMsgForSenderBySender is SealedRumorEvent) if (unwrappedMsgForSenderBySender is SealedRumorEvent) { - unwrappedMsgForSenderBySender.unseal(sender) { unwrappedRumorToSenderBySender -> - assertEquals("Hi There!", unwrappedRumorToSenderBySender.content) - countDownDecryptLatch.countDown() - } + val unwrappedRumorToSenderBySender = unwrappedMsgForSenderBySender.unsealThrowing(sender) + assertEquals("Hi There!", unwrappedRumorToSenderBySender.content) - unwrappedMsgForSenderBySender.unseal(receiver) { _ -> - fail( - "Should not be able to decrypt msg for the sender by the sender but decrypted with receiver", - ) + unwrappedMsgForSenderBySender.unsealOrNull(receiver)?.let { _ -> + fail("Should not be able to decrypt msg for the sender by the sender but decrypted with receiver") } } - } - giftWrapEventToReceiver!!.unwrap(sender) { _ -> - fail("Should not be able to decrypt msg for the receiver decrypted by the sender") - } + giftWrapToReceiver.unwrapOrNull(sender)?.let { _ -> + fail("Should not be able to decrypt msg for the receiver decrypted by the sender") + } - giftWrapEventToSender!!.unwrap(receiver) { _ -> - fail("Should not be able to decrypt msg for the sender decrypted by the receiver") - } + giftWrapToReceiver.unwrapOrNull(receiver)?.let { _ -> + fail("Should not be able to decrypt msg for the sender decrypted by the receiver") + } - giftWrapEventToReceiver!!.unwrap(receiver) { unwrappedMsgForReceiverByReceiver -> + val unwrappedMsgForReceiverByReceiver = giftWrapToReceiver.unwrapThrowing(receiver) assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverByReceiver.kind) assertTrue(unwrappedMsgForReceiverByReceiver is SealedRumorEvent) if (unwrappedMsgForReceiverByReceiver is SealedRumorEvent) { - unwrappedMsgForReceiverByReceiver.unseal(receiver) { unwrappedRumorToReceiverByReceiver -> - assertEquals("Hi There!", unwrappedRumorToReceiverByReceiver?.content) - countDownDecryptLatch.countDown() - } + val unwrappedRumorToReceiverByReceiver = unwrappedMsgForReceiverByReceiver.unsealThrowing(receiver) + assertEquals("Hi There!", unwrappedRumorToReceiverByReceiver.content) - unwrappedMsgForReceiverByReceiver.unseal(sender) { unwrappedRumorToReceiverBySender -> + unwrappedMsgForReceiverByReceiver.unsealOrNull(sender)?.let { _ -> fail( "Should not be able to decrypt msg for the receiver by the receiver but decrypted with the sender", ) @@ -296,227 +257,194 @@ class GiftWrapEventTest { } } - assertTrue(countDownDecryptLatch.await(1, TimeUnit.SECONDS)) - } - @Test() - fun testInternalsGroupMessage() { - val sender = NostrSignerInternal(KeyPair()) - val receiverA = NostrSignerInternal(KeyPair()) - val receiverB = NostrSignerInternal(KeyPair()) + fun testInternalsGroupMessage() = + runBlocking { + val sender = NostrSignerInternal(KeyPair()) + val receiverA = NostrSignerInternal(KeyPair()) + val receiverB = NostrSignerInternal(KeyPair()) - val countDownLatch = CountDownLatch(3) + val senderMessage = + sender.sign( + ChatMessageEvent.build( + msg = "Who is going to the party tonight?", + to = listOf(PTag(receiverA.pubKey), PTag(receiverB.pubKey)), + ), + ) - var giftWrapEventToSender: GiftWrapEvent? = null - var giftWrapEventToReceiverA: GiftWrapEvent? = null - var giftWrapEventToReceiverB: GiftWrapEvent? = null + val msgFromSenderToReceiverA = + SealedRumorEvent.create( + event = senderMessage, + encryptTo = receiverA.pubKey, + signer = sender, + ) - sender.sign( - ChatMessageEvent.build( - msg = "Who is going to the party tonight?", - to = listOf(PTag(receiverA.pubKey), PTag(receiverB.pubKey)), - ), - ) { senderMessage -> - SealedRumorEvent.create( - event = senderMessage, - encryptTo = receiverA.pubKey, - signer = sender, - ) { msgFromSenderToReceiverA -> - // Should expose sender - assertEquals(msgFromSenderToReceiverA.pubKey, sender.pubKey) - // Should not expose receiver - assertTrue(msgFromSenderToReceiverA.tags.isEmpty()) + // Should expose sender + assertEquals(msgFromSenderToReceiverA.pubKey, sender.pubKey) + // Should not expose receiver + assertTrue(msgFromSenderToReceiverA.tags.isEmpty()) + val giftWrapForReceiverA = GiftWrapEvent.create( event = msgFromSenderToReceiverA, recipientPubKey = receiverA.pubKey, - ) { giftWrapForReceiverA -> - // Should not be signed by neither sender nor receiver - assertNotEquals(giftWrapForReceiverA.pubKey, sender.pubKey) - assertNotEquals(giftWrapForReceiverA.pubKey, receiverA.pubKey) - assertNotEquals(giftWrapForReceiverA.pubKey, receiverB.pubKey) + ) - // Should not include sender as recipient - assertNotEquals(giftWrapForReceiverA.recipientPubKey(), sender.pubKey) + // Should not be signed by neither sender nor receiver + assertNotEquals(giftWrapForReceiverA.pubKey, sender.pubKey) + assertNotEquals(giftWrapForReceiverA.pubKey, receiverA.pubKey) + assertNotEquals(giftWrapForReceiverA.pubKey, receiverB.pubKey) - // Should be addressed to the receiver - assertEquals(giftWrapForReceiverA.recipientPubKey(), receiverA.pubKey) + // Should not include sender as recipient + assertNotEquals(giftWrapForReceiverA.recipientPubKey(), sender.pubKey) - giftWrapEventToReceiverA = giftWrapForReceiverA + // Should be addressed to the receiver + assertEquals(giftWrapForReceiverA.recipientPubKey(), receiverA.pubKey) - countDownLatch.countDown() - } - } + val msgFromSenderToReceiverB = + SealedRumorEvent.create( + event = senderMessage, + encryptTo = receiverB.pubKey, + signer = sender, + ) - SealedRumorEvent.create( - event = senderMessage, - encryptTo = receiverB.pubKey, - signer = sender, - ) { msgFromSenderToReceiverB -> - // Should expose sender - assertEquals(msgFromSenderToReceiverB.pubKey, sender.pubKey) - // Should not expose receiver - assertTrue(msgFromSenderToReceiverB.tags.isEmpty()) + // Should expose sender + assertEquals(msgFromSenderToReceiverB.pubKey, sender.pubKey) + // Should not expose receiver + assertTrue(msgFromSenderToReceiverB.tags.isEmpty()) + val giftWrapForReceiverB = GiftWrapEvent.create( event = msgFromSenderToReceiverB, recipientPubKey = receiverB.pubKey, - ) { giftWrapForReceiverB -> - // Should not be signed by neither sender nor receiver - assertNotEquals(giftWrapForReceiverB.pubKey, sender.pubKey) - assertNotEquals(giftWrapForReceiverB.pubKey, receiverA.pubKey) - assertNotEquals(giftWrapForReceiverB.pubKey, receiverB.pubKey) + ) - // Should not include sender as recipient - assertNotEquals(giftWrapForReceiverB.recipientPubKey(), sender.pubKey) + // Should not be signed by neither sender nor receiver + assertNotEquals(giftWrapForReceiverB.pubKey, sender.pubKey) + assertNotEquals(giftWrapForReceiverB.pubKey, receiverA.pubKey) + assertNotEquals(giftWrapForReceiverB.pubKey, receiverB.pubKey) - // Should be addressed to the receiver - assertEquals(giftWrapForReceiverB.recipientPubKey(), receiverB.pubKey) + // Should not include sender as recipient + assertNotEquals(giftWrapForReceiverB.recipientPubKey(), sender.pubKey) - giftWrapEventToReceiverB = giftWrapForReceiverB + // Should be addressed to the receiver + assertEquals(giftWrapForReceiverB.recipientPubKey(), receiverB.pubKey) - countDownLatch.countDown() - } - } + val msgFromSenderToSender = + SealedRumorEvent.create( + event = senderMessage, + encryptTo = sender.pubKey, + signer = sender, + ) - SealedRumorEvent.create( - event = senderMessage, - encryptTo = sender.pubKey, - signer = sender, - ) { msgFromSenderToSender -> - // Should expose sender - assertEquals(msgFromSenderToSender.pubKey, sender.pubKey) - // Should not expose receiver - assertTrue(msgFromSenderToSender.tags.isEmpty()) + // Should expose sender + assertEquals(msgFromSenderToSender.pubKey, sender.pubKey) + // Should not expose receiver + assertTrue(msgFromSenderToSender.tags.isEmpty()) + val giftWrapToSender = GiftWrapEvent.create( event = msgFromSenderToSender, recipientPubKey = sender.pubKey, - ) { giftWrapToSender -> - // Should not be signed by neither the sender, not the receiver - assertNotEquals(giftWrapToSender.pubKey, sender.pubKey) - assertNotEquals(giftWrapToSender.pubKey, receiverA.pubKey) - assertNotEquals(giftWrapToSender.pubKey, receiverB.pubKey) + ) - // Should not be addressed to the receiver - assertNotEquals(giftWrapToSender.recipientPubKey(), receiverA.pubKey) - assertNotEquals(giftWrapToSender.recipientPubKey(), receiverB.pubKey) - // Should be addressed to the sender - assertEquals(giftWrapToSender.recipientPubKey(), sender.pubKey) + // Should not be signed by neither the sender, not the receiver + assertNotEquals(giftWrapToSender.pubKey, sender.pubKey) + assertNotEquals(giftWrapToSender.pubKey, receiverA.pubKey) + assertNotEquals(giftWrapToSender.pubKey, receiverB.pubKey) - giftWrapEventToSender = giftWrapToSender + // Should not be addressed to the receiver + assertNotEquals(giftWrapToSender.recipientPubKey(), receiverA.pubKey) + assertNotEquals(giftWrapToSender.recipientPubKey(), receiverB.pubKey) + // Should be addressed to the sender + assertEquals(giftWrapToSender.recipientPubKey(), sender.pubKey) - countDownLatch.countDown() - } - } - } + // Done + // ----- + // Decrypting - // Done - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + // Receiver's side + // Makes sure it can only be decrypted by the target user - // Receiver's side - // Makes sure it can only be decrypted by the target user + assertNotNull(giftWrapToSender) + assertNotNull(giftWrapForReceiverA) + assertNotNull(giftWrapForReceiverB) - assertNotNull(giftWrapEventToSender) - assertNotNull(giftWrapEventToReceiverA) - assertNotNull(giftWrapEventToReceiverB) - - val countDownDecryptLatch = CountDownLatch(3) - - giftWrapEventToSender?.unwrap(sender) { unwrappedMsgForSenderBySender -> + val unwrappedMsgForSenderBySender = giftWrapToSender.unwrapThrowing(sender) assertEquals(SealedRumorEvent.KIND, unwrappedMsgForSenderBySender.kind) if (unwrappedMsgForSenderBySender is SealedRumorEvent) { - unwrappedMsgForSenderBySender.unseal(receiverA) { unwrappedRumorToSenderByReceiverA -> + unwrappedMsgForSenderBySender.unsealOrNull(receiverA)?.let { _ -> fail() } - unwrappedMsgForSenderBySender.unseal(receiverB) { unwrappedRumorToSenderByReceiverB -> + unwrappedMsgForSenderBySender.unsealOrNull(receiverB)?.let { _ -> fail() } - unwrappedMsgForSenderBySender.unseal(sender) { unwrappedRumorToSenderBySender -> - assertEquals( - "Who is going to the party tonight?", - unwrappedRumorToSenderBySender.content, - ) - } + val unwrappedRumorToSenderBySender = unwrappedMsgForSenderBySender.unsealThrowing(sender) + assertEquals("Who is going to the party tonight?", unwrappedRumorToSenderBySender.content) } - countDownDecryptLatch.countDown() - } + giftWrapForReceiverA.unwrapOrNull(sender)?.let { unwrappedMsgForReceiverBySenderA -> + fail("Should not be able to decode msg to the receiver A with the sender's key") + } - giftWrapEventToReceiverA!!.unwrap(sender) { unwrappedMsgForReceiverBySenderA -> - fail("Should not be able to decode msg to the receiver A with the sender's key") - } + giftWrapForReceiverB.unwrapOrNull(sender)?.let { unwrappedMsgForReceiverBySenderB -> + fail("Should not be able to decode msg to the receiver B with the sender's key") + } - giftWrapEventToReceiverB!!.unwrap(sender) { unwrappedMsgForReceiverBySenderB -> - fail("Should not be able to decode msg to the receiver B with the sender's key") - } + giftWrapToSender.unwrapOrNull(receiverA)?.let { + fail("Should not be able to decode msg to sender with the receiver A's key") + } - giftWrapEventToSender!!.unwrap(receiverA) { - fail("Should not be able to decode msg to sender with the receiver A's key") - } - - giftWrapEventToReceiverA!!.unwrap(receiverA) { unwrappedMsgForReceiverAByReceiverA -> + val unwrappedMsgForReceiverAByReceiverA = giftWrapForReceiverA.unwrapThrowing(receiverA) assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverAByReceiverA.kind) if (unwrappedMsgForReceiverAByReceiverA is SealedRumorEvent) { - unwrappedMsgForReceiverAByReceiverA.unseal(receiverA) { unwrappedRumorToReceiverAByReceiverA -> - assertEquals( - "Who is going to the party tonight?", - unwrappedRumorToReceiverAByReceiverA.content, - ) - } + val unwrappedRumorToReceiverAByReceiverA = unwrappedMsgForReceiverAByReceiverA.unsealThrowing(receiverA) + assertEquals("Who is going to the party tonight?", unwrappedRumorToReceiverAByReceiverA.content) - unwrappedMsgForReceiverAByReceiverA.unseal(sender) { unwrappedRumorToReceiverABySender -> + unwrappedMsgForReceiverAByReceiverA.unsealOrNull(sender)?.let { unwrappedRumorToReceiverABySender -> fail() } - unwrappedMsgForReceiverAByReceiverA.unseal(receiverB) { unwrappedRumorToReceiverAByReceiverB -> + unwrappedMsgForReceiverAByReceiverA.unsealOrNull(receiverB)?.let { unwrappedRumorToReceiverAByReceiverB -> fail() } } - countDownDecryptLatch.countDown() - } + giftWrapForReceiverB.unwrapOrNull(receiverA)?.let { + fail("Should not be able to decode msg to sender with the receiver A's key") + } - giftWrapEventToReceiverB!!.unwrap(receiverA) { - fail("Should not be able to decode msg to sender with the receiver A's key") - } + giftWrapToSender.unwrapOrNull(receiverB)?.let { unwrappedMsgForSenderByReceiverB -> + fail("Should not be able to decode msg to sender with the receiver B's key") + } + giftWrapForReceiverA.unwrapOrNull(receiverB)?.let { unwrappedMsgForReceiverAByReceiverB -> + fail("Should not be able to decode msg to receiver A with the receiver B's key") + } - giftWrapEventToSender!!.unwrap(receiverB) { unwrappedMsgForSenderByReceiverB -> - fail("Should not be able to decode msg to sender with the receiver B's key") - } - giftWrapEventToReceiverA!!.unwrap(receiverB) { unwrappedMsgForReceiverAByReceiverB -> - fail("Should not be able to decode msg to receiver A with the receiver B's key") - } - giftWrapEventToReceiverB!!.unwrap(receiverB) { unwrappedMsgForReceiverBByReceiverB -> + val unwrappedMsgForReceiverBByReceiverB = giftWrapForReceiverB.unwrapThrowing(receiverB) assertEquals(SealedRumorEvent.KIND, unwrappedMsgForReceiverBByReceiverB.kind) if (unwrappedMsgForReceiverBByReceiverB is SealedRumorEvent) { - unwrappedMsgForReceiverBByReceiverB.unseal(receiverA) { unwrappedRumorToReceiverBByReceiverA -> + unwrappedMsgForReceiverBByReceiverB.unsealOrNull(receiverA)?.let { unwrappedRumorToReceiverBByReceiverA -> fail() } - unwrappedMsgForReceiverBByReceiverB.unseal(receiverB) { unwrappedRumorToReceiverBByReceiverB -> - assertEquals( - "Who is going to the party tonight?", - unwrappedRumorToReceiverBByReceiverB.content, - ) + val unwrappedRumorToReceiverBByReceiverB = unwrappedMsgForReceiverBByReceiverB.unsealThrowing(receiverB) + assertEquals( + "Who is going to the party tonight?", + unwrappedRumorToReceiverBByReceiverB.content, + ) - countDownDecryptLatch.countDown() - } - - unwrappedMsgForReceiverBByReceiverB.unseal(sender) { unwrappedRumorToReceiverBBySender -> + unwrappedMsgForReceiverBByReceiverB.unsealOrNull(sender)?.let { unwrappedRumorToReceiverBBySender -> fail() } } } - assertTrue(countDownDecryptLatch.await(1, TimeUnit.SECONDS)) - } - @Test fun testCaseFromAmethyst1() { val json = @@ -537,18 +465,14 @@ class GiftWrapEventTest { } """.trimIndent() - var rumor: Event? = null - - wait1SecondForResult { onDone -> - val privateKey = "de6152a85a0dea3b09a08a6f8139a314d498a7b52f7e5c28858b64270abd4c70" - unwrapUnsealRumor(json, privateKey) { - rumor = it - onDone() + val rumor: Event = + runBlocking { + val privateKey = "de6152a85a0dea3b09a08a6f8139a314d498a7b52f7e5c28858b64270abd4c70" + unwrapUnsealRumor(json, privateKey) } - } assertNotNull(rumor) - assertEquals("Hola, que tal?", rumor?.content) + assertEquals("Hola, que tal?", rumor.content) } @Test @@ -573,17 +497,13 @@ class GiftWrapEventTest { val privateKey = "409ff7654141eaa16cd2161fe5bd127aeaef71f270c67587474b78998a8e3533" - var rumor: Event? = null - - wait1SecondForResult { onDone -> - unwrapUnsealRumor(json, privateKey) { - rumor = it - onDone() + val rumor: Event = + runBlocking { + unwrapUnsealRumor(json, privateKey) } - } assertNotNull(rumor) - assertEquals("Hola, que tal?", rumor?.content) + assertEquals("Hola, que tal?", rumor.content) } @Test @@ -610,17 +530,14 @@ class GiftWrapEventTest { """.trimIndent() val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771" - var rumor: Event? = null - wait1SecondForResult { onDone -> - unwrapUnsealRumor(json, privateKey) { - rumor = it - onDone() + val rumor: Event = + runBlocking { + unwrapUnsealRumor(json, privateKey) } - } assertNotNull(rumor) - assertEquals("test", rumor?.content) + assertEquals("test", rumor.content) } @Test @@ -645,21 +562,17 @@ class GiftWrapEventTest { val privateKey = "09e0051fdf5fdd9dd7a54713583006442cbdbf87bdcdab1a402f26e527d56771" - var rumor: Event? = null - - wait1SecondForResult { onDone -> - unwrapUnsealRumor(json, privateKey) { - rumor = it - onDone() + val rumor = + runBlocking { + unwrapUnsealRumor(json, privateKey) } - } - assertEquals("asdfasdfasdf", rumor?.content) - assertEquals(1690659269L, rumor?.createdAt) - assertEquals("827ba09d32ab81d62c60f657b350198c8aaba84372dab9ad3f4f6b8b7274b707", rumor?.id) - assertEquals(14, rumor?.kind) - assertEquals("subject", rumor?.tags?.firstOrNull()?.get(0)) - assertEquals("test", rumor?.tags?.firstOrNull()?.get(1)) + assertEquals("asdfasdfasdf", rumor.content) + assertEquals(1690659269L, rumor.createdAt) + assertEquals("827ba09d32ab81d62c60f657b350198c8aaba84372dab9ad3f4f6b8b7274b707", rumor.id) + assertEquals(14, rumor.kind) + assertEquals("subject", rumor.tags.firstOrNull()?.get(0)) + assertEquals("test", rumor.tags.firstOrNull()?.get(1)) } @Test @@ -684,33 +597,28 @@ class GiftWrapEventTest { val privateKey = "7dd22cafc512c0bc363a259f6dcda515b13ae3351066d7976fd0bb79cbd0d700" - var rumor: Event? = null - - wait1SecondForResult { onDone -> - unwrapUnsealRumor(json, privateKey) { - rumor = it - onDone() + val rumor = + runBlocking { + unwrapUnsealRumor(json, privateKey) } - } - assertEquals("8d1a56008d4e31dae2fb8bef36b3efea519eff75f57033107e2aa16702466ef2", rumor?.id) - assertEquals("Howdy", rumor?.content) - assertEquals(1690833960L, rumor?.createdAt) - assertEquals(14, rumor?.kind) - assertEquals("p", rumor?.tags?.firstOrNull()?.get(0)) + assertEquals("8d1a56008d4e31dae2fb8bef36b3efea519eff75f57033107e2aa16702466ef2", rumor.id) + assertEquals("Howdy", rumor.content) + assertEquals(1690833960L, rumor.createdAt) + assertEquals(14, rumor.kind) + assertEquals("p", rumor.tags.firstOrNull()?.get(0)) assertEquals( "b08d8857a92b4d6aa580ff55cc3c18c4edf313c83388c34abc118621f74f1a78", - rumor?.tags?.firstOrNull()?.get(1), + rumor.tags.firstOrNull()?.get(1), ) - assertEquals("subject", rumor?.tags?.getOrNull(1)?.get(0)) - assertEquals("Stuff", rumor?.tags?.getOrNull(1)?.get(1)) + assertEquals("subject", rumor.tags.getOrNull(1)?.get(0)) + assertEquals("Stuff", rumor.tags.getOrNull(1)?.get(1)) } - fun unwrapUnsealRumor( + suspend fun unwrapUnsealRumor( json: String, privateKey: HexKey, - onReady: (Event) -> Unit, - ) { + ): Event { val pkBytes = NostrSignerInternal(KeyPair(privateKey.hexToByteArray())) val wrap = Event.fromJson(json) as GiftWrapEvent @@ -718,13 +626,13 @@ class GiftWrapEventTest { assertEquals(pkBytes.pubKey, wrap.recipientPubKey()) - wrap.unwrap(pkBytes) { event -> - if (event is SealedRumorEvent) { - event.unseal(pkBytes, onReady) - } else { - println(event.toJson()) - fail("Event is not a Sealed Rumor") - } + val event = wrap.unwrapThrowing(pkBytes) + return if (event is SealedRumorEvent) { + event.unsealThrowing(pkBytes) + } else { + println(event.toJson()) + fail("Event is not a Sealed Rumor") + throw Exception("Event is not a Sealed Rumor") } } @@ -750,23 +658,11 @@ class GiftWrapEventTest { val wrap = Event.fromJson(msg) as GiftWrapEvent wrap.checkSignature() - var event: Event? = null - - wait1SecondForResult { onDone -> - wrap.unwrap(receiversPrivateKey) { - event = it - onDone() + val event = + runBlocking { + wrap.unwrapThrowing(receiversPrivateKey) } - } assertNotNull(event) } } - -fun wait1SecondForResult(run: (onDone: () -> Unit) -> Unit) { - val countDownLatch = CountDownLatch(1) - - run { countDownLatch.countDown() } - - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index acf2c7df07..0f14d4abcd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.quartz import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.edits.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent @@ -68,22 +67,23 @@ import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BlockedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.BookmarkListEvent -import com.vitorpamplona.quartz.nip51Lists.FollowListEvent -import com.vitorpamplona.quartz.nip51Lists.MuteListEvent -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip51Lists.RelaySetEvent -import com.vitorpamplona.quartz.nip51Lists.TrustedRelayListEvent -import com.vitorpamplona.quartz.nip51Lists.interests.HashtagListEvent -import com.vitorpamplona.quartz.nip51Lists.locations.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent +import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarRSVPEvent @@ -124,19 +124,30 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent +interface EventBuilder { + fun build( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, + ): Event +} + class EventFactory { companion object { - val factories: MutableMap>, String, HexKey) -> Event> = mutableMapOf() + val factories: MutableMap = mutableMapOf() - fun create( - id: String, - pubKey: String, + fun create( + id: HexKey, + pubKey: HexKey, createdAt: Long, kind: Int, tags: Array>, content: String, - sig: String, - ): Event = + sig: HexKey, + ): T = when (kind) { AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig) AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig) @@ -265,12 +276,9 @@ class EventFactory { VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig) WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig) else -> { - factories[kind]?.let { - return it(id, pubKey, createdAt, tags, content, sig) - } - - Event(id, pubKey, createdAt, kind, tags, content, sig) + factories[kind]?.build(id, pubKey, createdAt, tags, content, sig) + ?: Event(id, pubKey, createdAt, kind, tags, content, sig) } - } + } as T } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/decoupling/DecoupledCipher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/decoupling/DecoupledCipher.kt index f0f0d5bf89..d41537de40 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/decoupling/DecoupledCipher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/decoupling/DecoupledCipher.kt @@ -45,68 +45,62 @@ class DecoupledCipher { pubKey = fromPublicKey.hexToByteArray(), ) - fun encrypt( + suspend fun encrypt( decryptedContent: String, toPublicKey: HexKey, fromKeyList: EncryptionKeyListEvent, toKeyList: EncryptionKeyListEvent, signer: NostrSigner, - onReady: (String) -> Unit, - ) { + ): String? { val toKeys = toKeyList.keys() val sendToKey = if (toKeys.isEmpty()) toKeyList.pubKey else toKeys.random().pubkey val fromKeys = fromKeyList.keys() // uses the main key - if (fromKeys.isEmpty()) { - signer.nip44Encrypt(decryptedContent, sendToKey, onReady) + return if (fromKeys.isEmpty()) { + signer.nip44Encrypt(decryptedContent, sendToKey) } else { val keyToUse = fromKeys.random() - EncryptionKeyCache.getOrLoad( - deriveFromPubKey = signer.pubKey, - nonce = keyToUse.nonce, - load = { onLoaded -> - signer.deriveKey(keyToUse.nonce) { newPrivKey -> - onLoaded(newPrivKey.hexToByteArray()) - } - }, - ) { derivedPrivKey -> - onReady(innerEncrypt(decryptedContent, derivedPrivKey, sendToKey)) - } + EncryptionKeyCache + .getOrLoad( + deriveFromPubKey = signer.pubKey, + nonce = keyToUse.nonce, + load = { signer.deriveKey(keyToUse.nonce).hexToByteArray() }, + )?.let { derivedPrivKey -> + return innerEncrypt(decryptedContent, derivedPrivKey, sendToKey) + } } } - fun decrypt( + suspend fun decrypt( encryptedContent: String, fromPublicKey: HexKey, toPublicKey: HexKey, fromKeyList: EncryptionKeyListEvent, toEncryptedKeyList: EncryptionKeyListEvent, signer: NostrSigner, - onReady: (String) -> Unit, - ) { + ): String? { val fromKeys = fromKeyList.keys() val sentFromKey = if (fromKeys.isEmpty()) fromKeyList.pubKey else fromKeys.random().pubkey val keyToUse = toEncryptedKeyList.keys().firstOrNull { it.pubkey == toPublicKey } // uses the main key - if (signer.pubKey == toPublicKey) { - signer.nip44Decrypt(encryptedContent, sentFromKey, onReady) + return if (signer.pubKey == toPublicKey) { + signer.nip44Decrypt(encryptedContent, sentFromKey) } else if (keyToUse != null) { - EncryptionKeyCache.getOrLoad( - deriveFromPubKey = signer.pubKey, - nonce = keyToUse.nonce, - load = { onLoaded -> - signer.deriveKey(keyToUse.nonce) { newPrivKey -> - onLoaded(newPrivKey.hexToByteArray()) - } - }, - ) { derivedPrivKey -> - innerDecrypt(encryptedContent, derivedPrivKey, sentFromKey)?.let { onReady(it) } - } + EncryptionKeyCache + .getOrLoad( + deriveFromPubKey = signer.pubKey, + nonce = keyToUse.nonce, + load = { signer.deriveKey(keyToUse.nonce).hexToByteArray() }, + )?.let { derivedPrivKey -> + innerDecrypt(encryptedContent, derivedPrivKey, sentFromKey) + } + } else { + null } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/decoupling/EncryptionKeyCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/decoupling/EncryptionKeyCache.kt index 5c5308380a..0a812043ff 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/decoupling/EncryptionKeyCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/decoupling/EncryptionKeyCache.kt @@ -35,29 +35,29 @@ object EncryptionKeyCache { deriveFromPubKey: HexKey, nonce: HexKey, privKey: ByteArray, - ) = sharedNonceKeyCache.put(idx(deriveFromPubKey, nonce), privKey) + ): ByteArray? = sharedNonceKeyCache.put(idx(deriveFromPubKey, nonce), privKey) fun get( deriveFromPubKey: HexKey, nonce: HexKey, - ) = sharedNonceKeyCache.get(idx(deriveFromPubKey, nonce)) + ): ByteArray? = sharedNonceKeyCache.get(idx(deriveFromPubKey, nonce)) inline fun getOrLoad( deriveFromPubKey: HexKey, nonce: HexKey, - load: (onLoaded: (privKey: ByteArray) -> Unit) -> Unit, - crossinline whenReady: (privKey: ByteArray) -> Unit, - ) { + load: () -> ByteArray?, + ): ByteArray? { val cachedPrivKey = get(deriveFromPubKey, nonce) if (cachedPrivKey != null) { - whenReady(cachedPrivKey) - return + return cachedPrivKey } - load { newPrivKey -> + val newPrivKey = load() + newPrivKey?.let { put(deriveFromPubKey, nonce, newPrivKey) - whenReady(newPrivKey) + newPrivKey } + return newPrivKey } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt deleted file mode 100644 index 50f10e1949..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/PrivateOutboxRelayListEvent.kt +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.experimental.edits - -import android.util.Log -import androidx.compose.runtime.Immutable -import com.fasterxml.jackson.module.kotlin.readValue -import com.vitorpamplona.quartz.experimental.edits.tags.RelayTag -import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.TagArray -import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri -import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - -@Immutable -class PrivateOutboxRelayListEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient private var privateTagsCache: Array>? = null - - override fun isContentEncoded() = true - - override fun countMemory(): Long = - super.countMemory() + - pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0) - - fun relays(): List? = - tags - .mapNotNull(RelayTag::parse) - .plus( - privateTagsCache?.mapNotNull(RelayTag::parse) ?: emptyList(), - ).ifEmpty { null } - - fun cachedPrivateTags(): Array>? = privateTagsCache - - fun privateTags( - signer: NostrSigner, - onReady: (Array>) -> Unit, - ) { - if (content.isEmpty()) { - onReady(emptyArray()) - return - } - - privateTagsCache?.let { - onReady(it) - return - } - - try { - signer.nip44Decrypt(content, pubKey) { - try { - privateTagsCache = JsonMapper.mapper.readValue(it) - privateTagsCache?.let { onReady(it) } - } catch (e: Throwable) { - Log.w("PrivateOutboxRelayListEvent", "Error parsing the JSON: ${e.message}. Json `$it` from event `${toNostrUri()}`") - } - } - } catch (e: Throwable) { - Log.w("PrivateOutboxRelayListEvent", "Error decrypting content: ${e.message}. Event: `${toNostrUri()}`") - } - } - - companion object { - const val KIND = 10013 - val TAGS = arrayOf(AltTag.assemble("Relay list to store private content from this author")) - - fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) - - fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) - - fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) - - fun encryptTags( - privateTags: Array>? = null, - signer: NostrSigner, - onReady: (String) -> Unit, - ) { - val msg = JsonMapper.mapper.writeValueAsString(privateTags) - - signer.nip44Encrypt( - msg, - signer.pubKey, - onReady, - ) - } - - fun createTagArray(relays: List): Array> = - relays - .map { - RelayTag.assemble(it) - }.toTypedArray() - - fun updateRelayList( - earlierVersion: PrivateOutboxRelayListEvent, - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PrivateOutboxRelayListEvent) -> Unit, - ) { - val tags = - earlierVersion.privateTagsCache - ?.filter(RelayTag::notMatch) - ?.plus( - relays.map { - RelayTag.assemble(it) - }, - )?.toTypedArray() ?: emptyArray() - - encryptTags(tags, signer) { - signer.sign(createdAt, KIND, TAGS, it) { - it.privateTagsCache = tags - onReady(it) - } - } - } - - fun createFromScratch( - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PrivateOutboxRelayListEvent) -> Unit, - ) { - create(relays, signer, createdAt, onReady) - } - - fun create( - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PrivateOutboxRelayListEvent) -> Unit, - ) { - val privateTagArray = createTagArray(relays) - encryptTags(privateTagArray, signer) { privateTags -> - signer.sign(createdAt, KIND, TAGS, privateTags) { - it.privateTagsCache = privateTagArray - onReady(it) - } - } - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/TextNoteModificationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/TextNoteModificationEvent.kt index f0b09f6fc9..39e5a733d7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/TextNoteModificationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/TextNoteModificationEvent.kt @@ -45,15 +45,14 @@ class TextNoteModificationEvent( const val KIND = 1010 const val ALT = "Content Change Event" - fun create( + suspend fun create( content: String, eventId: HexKey, notify: HexKey?, summary: String?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (TextNoteModificationEvent) -> Unit, - ) { + ): TextNoteModificationEvent { val tags = mutableListOf(arrayOf("e", eventId)) notify?.let { @@ -66,7 +65,7 @@ class TextNoteModificationEvent( tags.add(AltTag.assemble(ALT)) - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + return signer.sign(createdAt, KIND, tags.toTypedArray(), content) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt index 884b19def1..42cc41a3c7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.experimental.ephemChat.chat +import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl @@ -39,4 +40,6 @@ data class RoomId( result } } + + fun toTagArray() = RoomIdTag.assemble(id, relayUrl) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt index 5ce927a9df..37bbaf1bb7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt @@ -22,15 +22,22 @@ package com.vitorpamplona.quartz.experimental.ephemChat.list import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId -import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag +import com.vitorpamplona.quartz.experimental.ephemChat.list.rooms import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastAny import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.remove import com.vitorpamplona.quartz.utils.TimeUtils +import java.lang.reflect.Modifier.isPrivate @Immutable class EphemeralChatListEvent( @@ -41,31 +48,6 @@ class EphemeralChatListEvent( content: String, sig: HexKey, ) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient var publicAndPrivateEventCache: Set? = null - - fun publicAndPrivateRoomIds( - signer: NostrSigner, - onReady: (Set) -> Unit, - ) { - publicAndPrivateEventCache?.let { eventList -> - onReady(eventList) - return - } - - privateTags(signer) { - val set = filterRooms(it) - publicAndPrivateEventCache = set - onReady(set) - } - } - - fun filterRooms(privateTags: Array>): Set { - val privateRooms = privateTags.mapNotNull(RoomIdTag::parse) - val publicRooms = tags.mapNotNull(RoomIdTag::parse) - - return (privateRooms + publicRooms).toSet() - } - companion object { const val KIND = 10023 const val ALT = "Ephemeral Chat List" @@ -73,76 +55,120 @@ class EphemeralChatListEvent( fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) - fun createRoom( + suspend fun create( room: RoomId, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (EphemeralChatListEvent) -> Unit, - ) { - val tags = arrayOf(RoomIdTag.assemble(room)) + ): EphemeralChatListEvent = if (isPrivate) { - PrivateTagsInContent.encryptNip04( - privateTags = tags, + create( + publicRooms = emptyList(), + privateRooms = listOf(room), signer = signer, - ) { encryptedTags -> - create(encryptedTags, emptyArray(), signer, createdAt, onReady) - } + createdAt = createdAt, + ) } else { - create("", tags, signer, createdAt, onReady) + create( + publicRooms = listOf(room), + privateRooms = emptyList(), + signer = signer, + createdAt = createdAt, + ) } - } - fun removeRoom( + suspend fun add( earlierVersion: EphemeralChatListEvent, room: RoomId, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (EphemeralChatListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.removeAll( - earlierVersion, - RoomIdTag.assemble(room.id, room.relayUrl), - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) + ): EphemeralChatListEvent = + if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + resign( + tags = earlierVersion.tags, + privateTags = privateTags.plus(room.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } else { + resign( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(room.toTagArray()), + signer = signer, + createdAt = createdAt, + ) } - } - fun addRoom( + suspend fun remove( earlierVersion: EphemeralChatListEvent, room: RoomId, - isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (EphemeralChatListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.add( - earlierVersion, - RoomIdTag.assemble(room.id, room.relayUrl.url), - isPrivate, - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } + ): EphemeralChatListEvent { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + return resign( + privateTags = privateTags.remove(room.toTagArray()), + tags = earlierVersion.tags.remove(room.toTagArray()), + signer = signer, + createdAt = createdAt, + ) } - fun create( + suspend fun resign( + tags: TagArray, + privateTags: TagArray, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = resign( + content = PrivateTagsInContent.encryptNip04(privateTags, signer), + tags = tags, + signer = signer, + createdAt = createdAt, + ) + + suspend fun resign( content: String, - tags: Array>, + tags: TagArray, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (EphemeralChatListEvent) -> Unit, - ) { + ): EphemeralChatListEvent { val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { + if (tags.fastAny(AltTag::match)) { tags } else { tags + AltTag.assemble(ALT) } - signer.sign(createdAt, KIND, newTags, content, onReady) + return signer.sign(createdAt, KIND, newTags, content) + } + + suspend fun create( + publicRooms: List = emptyList(), + privateRooms: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): EphemeralChatListEvent { + val template = build(publicRooms, privateRooms, signer, createdAt) + return signer.sign(template) + } + + suspend fun build( + publicRooms: List = emptyList(), + privateRooms: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip04(privateRooms.map { it.toTagArray() }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + rooms(publicRooms) + + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/TagArrayBuilderExt.kt index b0a6c3a55a..441cb8d6c7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/TagArrayBuilderExt.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.experimental.ephemChat.list +import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder @@ -27,3 +28,5 @@ fun TagArrayBuilder.roomId( id: String, relayUrl: String, ) = addUnique(RoomIdTag.assemble(id, relayUrl)) + +fun TagArrayBuilder.rooms(rooms: List) = addAll(rooms.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/TagArrayExt.kt new file mode 100644 index 0000000000..b45a43d502 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/TagArrayExt.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.ephemChat.list + +import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag +import com.vitorpamplona.quartz.nip01Core.core.TagArray + +fun TagArray.rooms() = mapNotNull(RoomIdTag::parse) + +fun TagArray.roomSet() = mapNotNullTo(mutableSetOf(), RoomIdTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/GalleryListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/GalleryListEvent.kt index a17ed3ef35..a6af5d84e0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/GalleryListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/profileGallery/GalleryListEvent.kt @@ -25,9 +25,10 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.utils.TimeUtils +@Deprecated("Replaced by NIP-68") @Immutable class GalleryListEvent( id: HexKey, @@ -36,108 +37,96 @@ class GalleryListEvent( tags: Array>, content: String, sig: HexKey, -) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { companion object { const val KIND = 10011 const val ALT = "Profile Gallery" - const val GALLERYTAGNAME = "url" + const val GALLERY_TAG_NAME = "url" - fun addEvent( + suspend fun addEvent( earlierVersion: GalleryListEvent?, eventId: HexKey, url: String, relay: String?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (GalleryListEvent) -> Unit, - ) = addTag(earlierVersion, GALLERYTAGNAME, eventId, url, relay, signer, createdAt, onReady) + ) = addTag(earlierVersion, GALLERY_TAG_NAME, eventId, url, relay, signer, createdAt) - fun addTag( + suspend fun addTag( earlierVersion: GalleryListEvent?, tagName: String, - eventid: HexKey, + eventId: HexKey, url: String, relay: String?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (GalleryListEvent) -> Unit, - ) { - val tags = arrayOf(tagName, url, eventid) + ): GalleryListEvent { + val tags = arrayOf(tagName, url, eventId) if (relay != null) { tags + relay } - add( + return add( earlierVersion, arrayOf(tags), signer, createdAt, - onReady, ) } - fun add( + suspend fun add( earlierVersion: GalleryListEvent?, listNewTags: Array>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (GalleryListEvent) -> Unit, - ) { + ): GalleryListEvent = create( content = earlierVersion?.content ?: "", tags = listNewTags.plus(earlierVersion?.tags ?: arrayOf()), signer = signer, createdAt = createdAt, - onReady = onReady, ) - } - fun removeEvent( + suspend fun removeEvent( earlierVersion: GalleryListEvent, eventId: HexKey, url: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (GalleryListEvent) -> Unit, - ) = removeTag(earlierVersion, GALLERYTAGNAME, eventId, url, signer, createdAt, onReady) + ) = removeTag(earlierVersion, GALLERY_TAG_NAME, eventId, url, signer, createdAt) - fun removeReplaceable( + suspend fun removeReplaceable( earlierVersion: GalleryListEvent, aTag: ATag, url: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (GalleryListEvent) -> Unit, - ) = removeTag(earlierVersion, GALLERYTAGNAME, aTag.toTag(), url, signer, createdAt, onReady) + ) = removeTag(earlierVersion, GALLERY_TAG_NAME, aTag.toTag(), url, signer, createdAt) - private fun removeTag( + private suspend fun removeTag( earlierVersion: GalleryListEvent, tagName: String, - eventid: HexKey, + eventId: HexKey, url: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (GalleryListEvent) -> Unit, - ) { + ): GalleryListEvent = create( content = earlierVersion.content, tags = earlierVersion.tags - .filter { it.size <= 1 || !(it[0] == tagName && it[1] == url && it[2] == eventid) } + .filter { it.size <= 1 || !(it[0] == tagName && it[1] == url && it[2] == eventId) } .toTypedArray(), signer = signer, createdAt = createdAt, - onReady = onReady, ) - } - fun create( + suspend fun create( content: String, tags: Array>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (GalleryListEvent) -> Unit, - ) { + ): GalleryListEvent { val newTags = if (tags.any { it.size > 1 && it[0] == "alt" }) { tags @@ -145,7 +134,7 @@ class GalleryListEvent( tags + AltTag.assemble(ALT) } - signer.sign(createdAt, KIND, newTags, content, onReady) + return signer.sign(createdAt, KIND, newTags, content) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/RelationshipStatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/RelationshipStatusEvent.kt index ccfbf6db4d..ac22a1e95c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/RelationshipStatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/RelationshipStatusEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.experimental.relationshipStatus import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetnameTag +import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetNameTag import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.RankTag import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.SummaryTag import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -45,7 +45,7 @@ class RelationshipStatusEvent( ) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { fun rank() = tags.firstNotNullOfOrNull(RankTag::parse) - fun petname() = tags.firstNotNullOfOrNull(PetnameTag::parse) + fun petName() = tags.firstNotNullOfOrNull(PetNameTag::parse) fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) @@ -53,16 +53,15 @@ class RelationshipStatusEvent( const val KIND = 30382 const val ALT = "Relationship Status" - fun create( + suspend fun create( targetUser: HexKey, - petname: String? = null, + petName: String? = null, summary: String? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), publicInitializer: TagArrayBuilder.() -> Unit = {}, privateInitializer: TagArrayBuilder.() -> Unit = {}, - onReady: (RelationshipStatusEvent) -> Unit, - ) { + ): RelationshipStatusEvent { val publicTags = tagArray { alt(ALT) @@ -72,14 +71,13 @@ class RelationshipStatusEvent( val privateTags = tagArray { - petname?.let { petname(it) } + petName?.let { petName(it) } summary?.let { summary(it) } privateInitializer() } - PrivateTagsInContent.encryptNip44(privateTags, signer) { content -> - signer.sign(createdAt, KIND, publicTags, content, onReady) - } + val encryptedContent = PrivateTagsInContent.encryptNip44(privateTags, signer) + return signer.sign(createdAt, KIND, publicTags, encryptedContent) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/TagArrayBuilderExt.kt index fb68a96a61..58626823ad 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/TagArrayBuilderExt.kt @@ -20,13 +20,13 @@ */ package com.vitorpamplona.quartz.experimental.relationshipStatus -import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetnameTag +import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.PetNameTag import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.RankTag import com.vitorpamplona.quartz.experimental.relationshipStatus.tags.SummaryTag import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder fun TagArrayBuilder.rank(rank: Int) = add(RankTag.assemble(rank)) -fun TagArrayBuilder.petname(name: String) = add(PetnameTag.assemble(name)) +fun TagArrayBuilder.petName(name: String) = add(PetNameTag.assemble(name)) fun TagArrayBuilder.summary(summary: String) = add(SummaryTag.assemble(summary)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/PetnameTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/PetNameTag.kt similarity index 95% rename from quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/PetnameTag.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/PetNameTag.kt index 407024e027..32561ccd62 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/PetnameTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/relationshipStatus/tags/PetNameTag.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.experimental.relationshipStatus.tags import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.ensure -class PetnameTag { +class PetNameTag { companion object { const val TAG_NAME = "petname" @@ -36,6 +36,6 @@ class PetnameTag { } @JvmStatic - fun assemble(petname: String) = arrayOf(TAG_NAME, petname) + fun assemble(petName: String) = arrayOf(TAG_NAME, petName) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt index c8a9cc3be2..81e828a657 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/EventExt.kt @@ -41,17 +41,10 @@ fun Event.verifySignature(): Boolean { /** Checks if the ID is correct and then if the pubKey's secret key signed the event. */ fun Event.checkSignature() { if (!verifyId()) { - throw Exception( - """ - |Unexpected ID. - | Event: ${toJson()} - | Actual ID: $id - | Generated: ${generateId()} - """.trimIndent(), - ) + throw Exception("ID mismatch: our ID is ${generateId()} for event ${toJson()}") } if (!verifySignature()) { - throw Exception("""Bad signature!""") + throw Exception("Bad signature!") } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventAssembler.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventAssembler.kt index e16be1da7e..b6fd97e0f5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventAssembler.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventAssembler.kt @@ -48,7 +48,7 @@ class EventAssembler { tags, content, sig, - ) as T + ) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt index 36712c1586..b365d765dd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt @@ -43,7 +43,7 @@ import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorSerializer class JsonMapper { - companion object Companion { + companion object { val defaultPrettyPrinter = InliningTagArrayPrettyPrinter() val mapper = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt index d80ce69796..d8c51d9c29 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayAuthenticator.kt @@ -24,10 +24,13 @@ import android.util.Log import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch class RelayAuthenticator( val client: NostrClient, - val authenticate: (challenge: String, relay: IRelayClient) -> Unit, + val scope: CoroutineScope, + val authenticate: suspend (challenge: String, relay: IRelayClient) -> Unit, ) { private val clientListener = object : IRelayClientListener { @@ -35,7 +38,9 @@ class RelayAuthenticator( relay: IRelayClient, challenge: String, ) { - authenticate(challenge, relay) + scope.launch { + authenticate(challenge, relay) + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 698b81a0b8..761ef648f7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -144,7 +144,6 @@ open class BasicRelayClient( stats.newError(e.message ?: "Error trying to connect: ${e.javaClass.simpleName}") markConnectionAsClosed() - e.printStackTrace() } finally { connectingMutex.set(false) } @@ -224,7 +223,6 @@ open class BasicRelayClient( markConnectionAsClosed() Log.w(logTag, "OnFailure $code $response ${t.message} $socket") - t.printStackTrace() listener.onError( this@BasicRelayClient, "", diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt index c5e3635d0e..40dbd3a165 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSigner.kt @@ -20,10 +20,8 @@ */ package com.vitorpamplona.quartz.nip01Core.signers -import com.vitorpamplona.quartz.EventFactory import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent @@ -33,104 +31,49 @@ abstract class NostrSigner( ) { abstract fun isWriteable(): Boolean - fun sign( - ev: EventTemplate, - onReady: (T) -> Unit, - ) = sign(ev.createdAt, ev.kind, ev.tags, ev.content, onReady) + suspend fun sign(ev: EventTemplate): T = sign(ev.createdAt, ev.kind, ev.tags, ev.content) - abstract fun sign( + abstract suspend fun sign( createdAt: Long, kind: Int, tags: Array>, content: String, - onReady: (T) -> Unit, - ) + ): T - abstract fun nip04Encrypt( + abstract suspend fun nip04Encrypt( plaintext: String, toPublicKey: HexKey, - onReady: (String) -> Unit, - ) + ): String - abstract fun nip04Decrypt( + abstract suspend fun nip04Decrypt( ciphertext: String, fromPublicKey: HexKey, - onReady: (String) -> Unit, - ) + ): String - abstract fun nip44Encrypt( + abstract suspend fun nip44Encrypt( plaintext: String, toPublicKey: HexKey, - onReady: (String) -> Unit, - ) + ): String - abstract fun nip44Decrypt( + abstract suspend fun nip44Decrypt( ciphertext: String, fromPublicKey: HexKey, - onReady: (String) -> Unit, - ) + ): String - abstract fun decryptZapEvent( - event: LnZapRequestEvent, - onReady: (LnZapPrivateEvent) -> Unit, - ) + abstract suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent - abstract fun deriveKey( - nonce: HexKey, - onReady: (HexKey) -> Unit, - ) + abstract suspend fun deriveKey(nonce: HexKey): HexKey - fun decrypt( + suspend fun decrypt( encryptedContent: String, fromPublicKey: HexKey, - onReady: (String) -> Unit, - ) { - if (EncryptedInfo.isNIP04(encryptedContent)) { - nip04Decrypt(encryptedContent, fromPublicKey, onReady) + ): String { + if (encryptedContent.isBlank()) throw SignerExceptions.NothingToDecrypt() + + return if (EncryptedInfo.isNIP04(encryptedContent)) { + nip04Decrypt(encryptedContent, fromPublicKey) } else { - nip44Decrypt(encryptedContent, fromPublicKey, onReady) + nip44Decrypt(encryptedContent, fromPublicKey) } } - - fun assembleRumor( - ev: EventTemplate, - onReady: (T) -> Unit, - ) = assembleRumor(ev.createdAt, ev.kind, ev.tags, ev.content, onReady) - - fun assembleRumor( - createdAt: Long, - kind: Int, - tags: Array>, - content: String, - onReady: (T) -> Unit, - ) { - onReady( - EventFactory.create( - id = EventHasher.hashId(pubKey, createdAt, kind, tags, content), - pubKey = pubKey, - createdAt = createdAt, - kind = kind, - tags = tags, - content = content, - sig = "", - ) as T, - ) - } - - fun assembleRumor(ev: EventTemplate) = assembleRumor(ev.createdAt, ev.kind, ev.tags, ev.content) - - fun assembleRumor( - createdAt: Long, - kind: Int, - tags: Array>, - content: String, - ) = EventFactory.create( - id = EventHasher.hashId(pubKey, createdAt, kind, tags, content), - pubKey = pubKey, - createdAt = createdAt, - kind = kind, - tags = tags, - content = content, - sig = "", - ) as T } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt index d441e9193b..6c8992da67 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import kotlinx.coroutines.CancellationException class NostrSignerInternal( val keyPair: KeyPair, @@ -34,59 +35,68 @@ class NostrSignerInternal( override fun isWriteable(): Boolean = keyPair.privKey != null - override fun sign( + inline fun runWrapErrors(action: () -> T): T = + try { + action() + } catch (e: SignerExceptions) { + throw e + } catch (e: Exception) { + if (e is CancellationException) throw e + throw SignerExceptions.CouldNotPerformException("Could not sign event.", e) + } + + override suspend fun sign( createdAt: Long, kind: Int, tags: Array>, content: String, - onReady: (T) -> Unit, - ) { - signerSync.sign(createdAt, kind, tags, content)?.let { onReady(it) } - } + ): T = + runWrapErrors { + signerSync.sign(createdAt, kind, tags, content) + } - override fun nip04Encrypt( + override suspend fun nip04Encrypt( plaintext: String, toPublicKey: HexKey, - onReady: (String) -> Unit, - ) { - signerSync.nip04Encrypt(plaintext, toPublicKey)?.let { onReady(it) } - } + ): String = + runWrapErrors { + signerSync.nip04Encrypt(plaintext, toPublicKey) + } - override fun nip04Decrypt( + override suspend fun nip04Decrypt( ciphertext: String, fromPublicKey: HexKey, - onReady: (String) -> Unit, - ) { - signerSync.nip04Decrypt(ciphertext, fromPublicKey)?.let { onReady(it) } - } + ): String = + runWrapErrors { + signerSync.nip04Decrypt(ciphertext, fromPublicKey) + } - override fun nip44Encrypt( + override suspend fun nip44Encrypt( plaintext: String, toPublicKey: HexKey, - onReady: (String) -> Unit, - ) { - signerSync.nip44Encrypt(plaintext, toPublicKey)?.let { onReady(it) } - } + ): String = + runWrapErrors { + signerSync.nip44Encrypt(plaintext, toPublicKey) + } - override fun nip44Decrypt( + override suspend fun nip44Decrypt( ciphertext: String, fromPublicKey: HexKey, - onReady: (String) -> Unit, - ) { - signerSync.nip44Decrypt(ciphertext, fromPublicKey)?.let { onReady(it) } + ): String = + runWrapErrors { + signerSync.nip44Decrypt(ciphertext, fromPublicKey) + } + + override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent { + if (!event.isPrivateZap()) throw SignerExceptions.NothingToDecrypt() + + return runWrapErrors { + signerSync.decryptZapEvent(event) + } } - override fun decryptZapEvent( - event: LnZapRequestEvent, - onReady: (LnZapPrivateEvent) -> Unit, - ) { - signerSync.decryptZapEvent(event)?.let { onReady(it) } - } - - override fun deriveKey( - nonce: HexKey, - onReady: (HexKey) -> Unit, - ) { - signerSync.deriveKey(nonce)?.let { onReady(it) } - } + override suspend fun deriveKey(nonce: HexKey): HexKey = + runWrapErrors { + signerSync.deriveKey(nonce) + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt index 51f3f051aa..25c9410c33 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.signers -import android.util.Log import com.vitorpamplona.quartz.experimental.decoupling.EncryptionKeyDerivation import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -28,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.EventAssembler import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04 import com.vitorpamplona.quartz.nip44Encryption.Nip44 import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent @@ -45,8 +45,8 @@ class NostrSignerSync( kind: Int, tags: Array>, content: String, - ): T? { - if (keyPair.privKey == null) return null + ): T { + if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() return if (isUnsignedPrivateZapEvent(kind, tags)) { // this is a private zap @@ -68,71 +68,78 @@ class NostrSignerSync( kind: Int, tags: Array>, content: String, - ): T? { - if (keyPair.privKey == null) return null + ): T { + if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() return EventAssembler.hashAndSign(pubKey, createdAt, kind, tags, content, keyPair.privKey) } fun nip04Encrypt( - decryptedContent: String, + plaintext: String, toPublicKey: HexKey, - ): String? { - if (keyPair.privKey == null) return null + ): String { + if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() return Nip04.encrypt( - decryptedContent, + plaintext, keyPair.privKey, toPublicKey.hexToByteArray(), ) } fun nip04Decrypt( - encryptedContent: String, + ciphertext: String, fromPublicKey: HexKey, - ): String? { - if (keyPair.privKey == null) return null + ): String { + if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() + if (ciphertext.isBlank()) throw SignerExceptions.NothingToDecrypt() - return try { - Nip04.decrypt(encryptedContent, keyPair.privKey, fromPublicKey.hexToByteArray()) - } catch (e: Exception) { - Log.w("NIP04Decrypt", "Error decrypting the message ${e.message} on $encryptedContent") - null - } + return Nip04.decrypt(ciphertext, keyPair.privKey, fromPublicKey.hexToByteArray()) } fun nip44Encrypt( - decryptedContent: String, + plaintext: String, toPublicKey: HexKey, - ): String? { - if (keyPair.privKey == null) return null + ): String { + if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() return Nip44 .encrypt( - decryptedContent, + plaintext, keyPair.privKey, toPublicKey.hexToByteArray(), ).encodePayload() } fun nip44Decrypt( - encryptedContent: String, + ciphertext: String, fromPublicKey: HexKey, - ): String? { - if (keyPair.privKey == null) return null + ): String { + if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() + if (ciphertext.isBlank()) throw SignerExceptions.NothingToDecrypt() return Nip44.decrypt( - payload = encryptedContent, + payload = ciphertext, privateKey = keyPair.privKey, pubKey = fromPublicKey.hexToByteArray(), ) } - fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent? = PrivateZapRequestBuilder().decryptZapEvent(event, this) + fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent = PrivateZapRequestBuilder().decryptZapEvent(event, this) - fun deriveKey(nonce: HexKey): HexKey? { - if (keyPair.privKey == null) return null + fun deriveKey(nonce: HexKey): HexKey { + if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() return EncryptionKeyDerivation.derivePrivateKey(keyPair.privKey, nonce.hexToByteArray()).toHexKey() } + + suspend fun decrypt( + encryptedContent: String, + fromPublicKey: HexKey, + ): String = + if (EncryptedInfo.isNIP04(encryptedContent)) { + nip04Decrypt(encryptedContent, fromPublicKey) + } else { + nip44Decrypt(encryptedContent, fromPublicKey) + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/SignerExceptions.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/SignerExceptions.kt new file mode 100644 index 0000000000..317a60bcea --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/SignerExceptions.kt @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.signers + +sealed class SignerExceptions( + msg: String, + cause: Throwable? = null, +) : Exception(msg, cause) { + class ReadOnlyException : SignerExceptions("Signer is read-only") + + class UnauthorizedDecryptionException : SignerExceptions("Couldn't not decrypt the contents of this event.") + + class NothingToDecrypt : SignerExceptions("Ciphertext is Empty") + + class AutomaticallyUnauthorizedException( + msg: String, + cause: Throwable? = null, + ) : SignerExceptions(msg) + + class ManuallyUnauthorizedException( + msg: String, + cause: Throwable? = null, + ) : SignerExceptions(msg) + + class TimedOutException( + msg: String, + cause: Throwable? = null, + ) : SignerExceptions(msg) + + class CouldNotPerformException( + msg: String, + cause: Throwable? = null, + ) : SignerExceptions(msg, cause) + + class RunningOnBackgroundWithoutAutomaticPermissionException( + msg: String, + cause: Throwable? = null, + ) : SignerExceptions(msg) + + class SignerNotFoundException( + msg: String, + cause: Throwable? = null, + ) : SignerExceptions(msg) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/CacheResults.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/CacheResults.kt new file mode 100644 index 0000000000..cda531884c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/CacheResults.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.signers.caches + +sealed interface CacheResults { + class Success( + val value: T, + ) : CacheResults + + class DontTryAgain : CacheResults + + class NeedsForegroundActivityToTryAgain( + val after: Long, + ) : CacheResults + + class CanTryAgain( + val after: Long, + ) : CacheResults +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/DecryptCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/DecryptCache.kt new file mode 100644 index 0000000000..328a73a53b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/DecryptCache.kt @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.signers.caches + +import android.util.Log +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal +import com.vitorpamplona.quartz.utils.TimeUtils + +abstract class DecryptCache( + val signer: NostrSigner, +) { + var cache: CacheResults = CacheResults.CanTryAgain(0) + + fun preload(result: T) { + cache = CacheResults.Success(result) + } + + abstract suspend fun decryptAndParse( + event: I, + signer: NostrSigner, + ): T + + private suspend fun performDecrypt(input: I): T? { + try { + val response = decryptAndParse(input, signer) + cache = CacheResults.Success(response) + return response + } catch (e: SignerExceptions.NothingToDecrypt) { + Log.w("DecryptCache", "Nothing to decrypt", e) + // ciphertext is blank. Cancels everything. + cache = CacheResults.DontTryAgain() + } catch (e: SignerExceptions.AutomaticallyUnauthorizedException) { + Log.w("DecryptCache", "NothAutomaticallyUnauthorizedException", e) + // User has rejected this permission. Don't try again. + cache = CacheResults.DontTryAgain() + } catch (e: SignerExceptions.ManuallyUnauthorizedException) { + Log.w("DecryptCache", "ManuallyUnauthorizedException", e) + // User has rejected this permission. Don't try again. + cache = CacheResults.CanTryAgain(TimeUtils.tenSecondsFromNow()) + } catch (e: SignerExceptions.TimedOutException) { + Log.w("DecryptCache", "TimedOutException", e) + // User has did not reply to the approval request. Ignore until later time. + cache = CacheResults.CanTryAgain(TimeUtils.tenSecondsFromNow()) + } catch (e: SignerExceptions.CouldNotPerformException) { + Log.w("DecryptCache", "CouldNotPerformException", e) + // Decryption failed. This key might not be able to decrypt anything. Don't try again. + cache = CacheResults.DontTryAgain() + } catch (e: SignerExceptions.SignerNotFoundException) { + Log.w("DecryptCache", "SignerNotFoundException", e) + // Signer app was deleted. Not sure what to to. It should probably log off. + cache = CacheResults.DontTryAgain() + } catch (e: SignerExceptions.RunningOnBackgroundWithoutAutomaticPermissionException) { + Log.w("DecryptCache", "RunningOnBackgroundWithoutAutomaticPermissionException", e) + // App received a notifications, asked the signer to decrypt but the permission was not automatic. + // It needs the interface but does not have it. It needs to wait for an activity. + cache = CacheResults.NeedsForegroundActivityToTryAgain(TimeUtils.tenSecondsFromNow()) + } catch (e: com.fasterxml.jackson.core.JsonParseException) { + Log.w("DecryptCache", "JsonParseException", e) + // Decryption failed. This key might not be able to decrypt anything. Don't try again. + cache = CacheResults.DontTryAgain() + } catch (e: IllegalStateException) { + Log.w("DecryptCache", "IllegalStateException", e) + cache = CacheResults.DontTryAgain() + } catch (e: IllegalArgumentException) { + Log.w("DecryptCache", "IllegalArgumentException", e) + cache = CacheResults.DontTryAgain() + } + return null + } + + fun cached(): T? { + val cachedResult = cache + return if (cachedResult is CacheResults.Success) { + cachedResult.value + } else { + null + } + } + + suspend fun decrypt(input: I): T? { + val cachedResult = cache + return when (cachedResult) { + is CacheResults.Success -> cachedResult.value + is CacheResults.DontTryAgain -> null + is CacheResults.CanTryAgain -> { + if (TimeUtils.now() > cachedResult.after) { + performDecrypt(input) + } else { + null + } + } + is CacheResults.NeedsForegroundActivityToTryAgain<*> -> { + if (TimeUtils.now() > cachedResult.after && (signer !is NostrSignerExternal || signer.hasForegroundActivity())) { + performDecrypt(input) + } else { + null + } + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt index 22cdf14e3f..74a0ac49bc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt @@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes data class ATag( val kind: Int, val pubKeyHex: HexKey, - val dTag: String, + val dTag: String = "", val relay: NormalizedRelayUrl? = null, ) { constructor(address: Address, relayHint: NormalizedRelayUrl? = null) : this(address.kind, address.pubKeyHex, address.dTag, relayHint) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt index 93b2d48c09..358d53559c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt @@ -31,7 +31,7 @@ import com.vitorpamplona.quartz.utils.pointerSizeInBytes data class Address( val kind: Int, val pubKeyHex: HexKey, - val dTag: String, + val dTag: String = "", ) : Comparable
{ fun toValue() = assemble(kind, pubKeyHex, dTag) @@ -59,7 +59,7 @@ data class Address( fun assemble( kind: Int, pubKeyHex: HexKey, - dTag: String, + dTag: String = "", ) = "$kind:$pubKeyHex:$dTag" @JvmStatic diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt index 25811be7b4..4ad0717ac7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt @@ -94,12 +94,12 @@ class ContactListEvent( fun blockListFor(pubKeyHex: HexKey): String = "3:$pubKeyHex:" - fun createFromScratch( + suspend fun createFromScratch( followUsers: List = emptyList(), relayUse: Map? = emptyMap(), signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), - ): ContactListEvent? { + ): ContactListEvent { val content = relayUse?.let { RelaySet.assemble(it) } ?: "" val tags = @@ -109,13 +109,12 @@ class ContactListEvent( return signer.sign(createdAt, KIND, tags.toTypedArray(), content) } - fun createFromScratch( + suspend fun createFromScratch( followUsers: List, relayUse: Map?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { + ): ContactListEvent { val content = relayUse?.let { RelaySet.assemble(it) } ?: "" val tags = followUsers.map { it.toTagArray() } @@ -124,53 +123,47 @@ class ContactListEvent( tags = tags.toTypedArray(), signer = signer, createdAt = createdAt, - onReady = onReady, ) } - fun followUser( + suspend fun followUser( earlierVersion: ContactListEvent, pubKeyHex: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { - if (earlierVersion.isTaggedUser(pubKeyHex)) return + ): ContactListEvent { + if (earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion return create( content = earlierVersion.content, tags = earlierVersion.tags.plus(element = arrayOf("p", pubKeyHex)), signer = signer, createdAt = createdAt, - onReady = onReady, ) } - fun unfollowUser( + suspend fun unfollowUser( earlierVersion: ContactListEvent, pubKeyHex: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { - if (!earlierVersion.isTaggedUser(pubKeyHex)) return + ): ContactListEvent { + if (!earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion return create( content = earlierVersion.content, tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex }.toTypedArray(), signer = signer, createdAt = createdAt, - onReady = onReady, ) } - fun updateRelayList( + suspend fun updateRelayList( earlierVersion: ContactListEvent, relayUse: Map?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { + ): ContactListEvent { val content = relayUse?.let { RelaySet.assemble(it) } ?: "" return create( @@ -178,17 +171,15 @@ class ContactListEvent( tags = earlierVersion.tags, signer = signer, createdAt = createdAt, - onReady = onReady, ) } - fun create( + suspend fun create( content: String, tags: Array>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ContactListEvent) -> Unit, - ) { + ): ContactListEvent { val newTags = if (tags.any { it.size > 1 && it[0] == "alt" }) { tags @@ -196,7 +187,7 @@ class ContactListEvent( tags + AltTag.assemble(ALT) } - signer.sign(createdAt, KIND, newTags, content, onReady) + return signer.sign(createdAt, KIND, newTags, content) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt new file mode 100644 index 0000000000..0ae1903328 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip04Dm + +import androidx.collection.LruCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent + +class PrivateDMCache( + signer: NostrSigner, +) { + private val decryptionCache = + object : LruCache(10000) { + override fun create(key: PrivateDmEvent): PrivateDMDecryptCache? { + val canDecrypt = key.canDecrypt(signer.pubKey) + return if (key.content.isNotBlank() && canDecrypt) { + PrivateDMDecryptCache(signer) + } else { + null + } + } + } + + fun cachedDM(event: PrivateDmEvent): String? = decryptionCache[event]?.cached() + + suspend fun decryptDM(event: PrivateDmEvent) = decryptionCache[event]?.decrypt(event) +} + +class PrivateDMDecryptCache( + signer: NostrSigner, +) : DecryptCache(signer) { + override suspend fun decryptAndParse( + event: PrivateDmEvent, + signer: NostrSigner, + ) = event.decryptContent(signer) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt index 1b140d8a24..68d68a8051 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt @@ -26,7 +26,9 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag @@ -37,8 +39,6 @@ import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes import kotlinx.collections.immutable.persistentSetOf @Immutable @@ -50,21 +50,35 @@ class PrivateDmEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig), - ChatroomKeyable { - @Transient private var decryptedContent: Map = mapOf() + ChatroomKeyable, + PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) - override fun countMemory(): Long = - super.countMemory() + - pointerSizeInBytes + (decryptedContent.values.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() }) + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) override fun isContentEncoded() = true + fun canDecrypt(signer: NostrSigner) = canDecrypt(signer.pubKey) + + fun canDecrypt(signerPubKey: HexKey) = pubKey == signerPubKey || recipientPubKey() == signerPubKey + + suspend fun decryptContent(signer: NostrSigner): String { + if (!canDecrypt(signer.pubKey)) throw SignerExceptions.UnauthorizedDecryptionException() + + val retVal = signer.decrypt(content, talkingWith(signer.pubKey)) + return if (retVal.startsWith(NIP_18_ADVERTISEMENT)) { + retVal.substring(16) + } else { + retVal + } + } + /** * This may or may not be the actual recipient's pub key. The event is intended to look like a * nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used for * initial messages. */ - private fun recipientPubKey() = tags.firstNotNullOfOrNull(PTag::parseKey) + fun recipientPubKey() = tags.firstNotNullOfOrNull(PTag::parseKey) fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this) }?.getOrNull() @@ -79,43 +93,12 @@ class PrivateDmEvent( fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) verifiedRecipientPubKey() ?: pubKey else pubKey - override fun chatroomKey(toRemove: String): ChatroomKey = ChatroomKey(persistentSetOf(talkingWith(toRemove))) + override fun chatroomKey(toRemove: HexKey): ChatroomKey = ChatroomKey(persistentSetOf(talkingWith(toRemove))) - /** - * To be fully compatible with nip-04, we read e-tags that are in violation to nip-18. - * - * Nip-18 messages should refer to other events by inline references in the content like - * `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506). - */ fun replyTo() = tags.firstNotNullOfOrNull(MarkedETag::parseId) fun with(pubkeyHex: HexKey): Boolean = pubkeyHex == pubKey || tags.any(PTag::isTagged, pubkeyHex) - fun cachedContentFor(signer: NostrSigner): String? = decryptedContent[signer.pubKey] - - fun plainContent( - signer: NostrSigner, - onReady: (String) -> Unit, - ) { - decryptedContent[signer.pubKey]?.let { - onReady(it) - return - } - - signer.decrypt(content, talkingWith(signer.pubKey)) { retVal -> - val content = - if (retVal.startsWith(NIP_18_ADVERTISEMENT)) { - retVal.substring(16) - } else { - retVal - } - - decryptedContent = decryptedContent + Pair(signer.pubKey, content) - - onReady(content) - } - } - companion object { const val KIND = 4 const val ALT = "Private Message" @@ -123,11 +106,11 @@ class PrivateDmEvent( fun prepareMessageToEncrypt( msg: String, - imetas: List? = null, + iMetas: List? = null, advertiseNip18: Boolean = true, ): String { var message = msg - imetas?.forEach { + iMetas?.forEach { message = message.replace(it.url, Nip54InlineMetadata().createUrl(it.url, it.properties)) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt index 9506c16283..cccbde429b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip11RelayInfo/Nip11RelayInformation.kt @@ -48,7 +48,9 @@ class Nip11RelayInformation( ) { companion object { val mapper = - jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + jacksonObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) fun fromJson(json: String): Nip11RelayInformation = mapper.readValue(json, Nip11RelayInformation::class.java) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt index daeb751421..fa116d6793 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/NIP17Factory.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.utils.mapNotNullAsync class NIP17Factory { data class Result( @@ -38,122 +39,82 @@ class NIP17Factory { val wraps: List, ) - private fun recursiveGiftWrapCreation( - event: Event, - remainingTos: List, - signer: NostrSigner, - output: MutableList, - onReady: (List) -> Unit, - ) { - if (remainingTos.isEmpty()) { - onReady(output) - return - } - - val next = remainingTos.first() - - SealedRumorEvent.create( - event = event, - encryptTo = next, - signer = signer, - ) { seal -> - GiftWrapEvent.create( - event = seal, - recipientPubKey = next, - ) { giftWrap -> - output.add(giftWrap) - recursiveGiftWrapCreation(event, remainingTos.minus(next), signer, output, onReady) - } - } - } - - private fun createWraps( + private suspend fun createWraps( event: Event, to: Set, signer: NostrSigner, - onReady: (List) -> Unit, - ) { - val wraps = mutableListOf() - recursiveGiftWrapCreation(event, to.toList(), signer, wraps, onReady) - } + ): List = + mapNotNullAsync( + to.toList(), + ) { next -> + GiftWrapEvent.create( + event = + SealedRumorEvent.create( + event = event, + encryptTo = next, + signer = signer, + ), + recipientPubKey = next, + ) + } - fun createMessageNIP17( + suspend fun createMessageNIP17( template: EventTemplate, signer: NostrSigner, - onReady: (Result) -> Unit, - ) { - signer.sign(template) { senderMessage -> - createWraps(senderMessage, senderMessage.groupMembers(), signer) { wraps -> - onReady( - Result( - msg = senderMessage, - wraps = wraps, - ), - ) - } - } + ): Result { + val senderMessage = signer.sign(template) + val wraps = createWraps(senderMessage, senderMessage.groupMembers(), signer) + return Result( + msg = senderMessage, + wraps = wraps, + ) } - fun createEncryptedFileNIP17( + suspend fun createEncryptedFileNIP17( template: EventTemplate, signer: NostrSigner, - onReady: (Result) -> Unit, - ) { - signer.sign(template) { senderMessage -> - createWraps(senderMessage, senderMessage.groupMembers(), signer) { wraps -> - onReady( - Result( - msg = senderMessage, - wraps = wraps, - ), - ) - } - } + ): Result { + val senderMessage = signer.sign(template) + val wraps = createWraps(senderMessage, senderMessage.groupMembers(), signer) + + return Result( + msg = senderMessage, + wraps = wraps, + ) } - fun createReactionWithinGroup( + suspend fun createReactionWithinGroup( content: String, originalNote: EventHintBundle, to: List, signer: NostrSigner, - onReady: (Result) -> Unit, - ) { + ): Result { val senderPublicKey = signer.pubKey + val template = ReactionEvent.build(content, originalNote) - signer.sign( - ReactionEvent.build(content, originalNote), - ) { senderReaction -> - createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer) { wraps -> - onReady( - Result( - msg = senderReaction, - wraps = wraps, - ), - ) - } - } + val senderReaction = signer.sign(template) + val wraps = createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer) + return Result( + msg = senderReaction, + wraps = wraps, + ) } - fun createReactionWithinGroup( + suspend fun createReactionWithinGroup( emojiUrl: EmojiUrlTag, originalNote: EventHintBundle, to: List, signer: NostrSigner, - onReady: (Result) -> Unit, - ) { + ): Result { val senderPublicKey = signer.pubKey + val template = ReactionEvent.build(emojiUrl, originalNote) - signer.sign( - ReactionEvent.build(emojiUrl, originalNote), - ) { senderReaction -> - createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer) { wraps -> - onReady( - Result( - msg = senderReaction, - wraps = wraps, - ), - ) - } - } + val senderReaction = signer.sign(template) + val wraps = createWraps(senderReaction, to.plus(senderPublicKey).toSet(), signer) + + return Result( + msg = senderReaction, + wraps = wraps, + ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt index 807329a8b1..3b1ef9ecba 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip17Dm.base import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent @@ -68,6 +69,8 @@ open class BaseDMGroupEvent( return result } + override fun isIncluded(pubKey: HexKey) = tags.any(PTag::isTagged, pubKey) + override fun groupMembers() = recipientsPubKey().plus(pubKey).toSet() override fun chatroomKey(toRemove: String): ChatroomKey = ChatroomKey(talkingWith(toRemove).toImmutableSet()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt index 1889069745..fd45910be9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKey.kt @@ -26,4 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey @Stable data class ChatroomKey( val users: Set, -) +) : Comparable { + override fun compareTo(other: ChatroomKey): Int = users.hashCode().compareTo(other.users.hashCode()) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt index 865ba93cc0..0beca6658c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/NIP17Group.kt @@ -23,5 +23,7 @@ package com.vitorpamplona.quartz.nip17Dm.base import com.vitorpamplona.quartz.nip01Core.core.HexKey interface NIP17Group { + fun isIncluded(pubKey: HexKey): Boolean + fun groupMembers(): Set } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt index 40c9338344..8c44924c2c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/settings/ChatMessageRelayListEvent.kt @@ -54,19 +54,17 @@ class ChatMessageRelayListEvent( fun createTagArray(relays: List): Array> = relays - .map { - RelayTag.assemble(it) - }.plusElement( + .map { RelayTag.assemble(it) } + .plusElement( AltTag.assemble("Relay list to receive private messages"), ).toTypedArray() - fun updateRelayList( + suspend fun updateRelayList( earlierVersion: ChatMessageRelayListEvent, relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ChatMessageRelayListEvent) -> Unit, - ) { + ): ChatMessageRelayListEvent { val tags = earlierVersion.tags .filter(RelayTag::notMatch) @@ -76,31 +74,19 @@ class ChatMessageRelayListEvent( }, ).toTypedArray() - signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady) + return signer.sign(createdAt, KIND, tags, earlierVersion.content) } - fun createFromScratch( + suspend fun create( relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ChatMessageRelayListEvent) -> Unit, - ) { - create(relays, signer, createdAt, onReady) - } - - fun create( - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChatMessageRelayListEvent) -> Unit, - ) { - signer.sign(createdAt, KIND, createTagArray(relays), "", onReady) - } + ): ChatMessageRelayListEvent = signer.sign(createdAt, KIND, createTagArray(relays), "") fun create( relays: List, signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), - ): ChatMessageRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "") + ): ChatMessageRelayListEvent = signer.sign(createdAt, KIND, createTagArray(relays), "") } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt index a47bbd2753..0533215638 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt @@ -110,12 +110,11 @@ class GenericRepostEvent( initializer() } - fun create( + suspend fun create( boostedPost: Event, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (GenericRepostEvent) -> Unit, - ) { + ): GenericRepostEvent { val content = boostedPost.toJson() val tags = @@ -131,7 +130,7 @@ class GenericRepostEvent( tags.add(arrayOf("k", "${boostedPost.kind}")) tags.add(AltTag.assemble(ALT)) - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + return signer.sign(createdAt, KIND, tags.toTypedArray(), content) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt index d16627a6c4..3fe4138749 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NAddress.kt @@ -56,7 +56,6 @@ data class NAddress( } } catch (e: Throwable) { Log.w("NAddress", "Issue trying to Decode NIP19 $this: ${e.message}") - // e.printStackTrace() } return null diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NNote.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NNote.kt index 89bb869e8b..11bca830ed 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NNote.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NNote.kt @@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNote data class NNote( val hex: String, ) : Entity { - companion object Companion { + companion object { fun parse(bytes: ByteArray): NNote? { if (bytes.isEmpty()) return null return NNote(bytes.toHexKey()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyIdentifierTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyIdentifierTag.kt index 2c06d9faa6..a3c9a23edf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyIdentifierTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/tags/ReplyIdentifierTag.kt @@ -59,7 +59,7 @@ class ReplyIdentifierTag { fun matchOrNull( tag: Array, encodedScope: Set, - ) = if (tag.has(1) && tag[0] == RootIdentifierTag.Companion.TAG_NAME && tag[1] in encodedScope) { + ) = if (tag.has(1) && tag[0] == TAG_NAME && tag[1] in encodedScope) { tag[1] } else { null @@ -93,7 +93,7 @@ class ReplyIdentifierTag { @JvmStatic fun parseExternalId(tag: Tag): ExternalId? { ensure(tag.has(1)) { return null } - ensure(tag[0] == RootIdentifierTag.Companion.TAG_NAME) { return null } + ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } val value = tag[1] diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt index 384b541da8..18f3f373e5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt @@ -22,17 +22,23 @@ package com.vitorpamplona.quartz.nip28PublicChat.list import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint -import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.nip51Lists.removeAny import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -43,24 +49,11 @@ class ChannelListEvent( tags: Array>, content: String, sig: HexKey, -) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient var publicAndPrivateEventCache: Set? = null +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider { + override fun eventHints() = tags.mapNotNull(ChannelTag::parseAsHint) - fun publicAndPrivateChannels( - signer: NostrSigner, - onReady: (Set) -> Unit, - ) { - publicAndPrivateEventCache?.let { eventList -> - onReady(eventList) - return - } - - mergeTagList(signer) { - val set = it.mapNotNull(ETag::parseAsHint).toSet() - publicAndPrivateEventCache = set - onReady(set) - } - } + override fun linkedEventIds() = tags.mapNotNull(ChannelTag::parseId) companion object { const val KIND = 10005 @@ -69,170 +62,157 @@ class ChannelListEvent( fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) - private fun createChannelBase( - tags: Array>, + suspend fun create( + channel: ChannelTag, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ChannelListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.create( - tags, - isPrivate, - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - fun createChannel( - channel: EventHintBundle, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChannelListEvent) -> Unit, - ) = createChannelBase( - tags = arrayOf(ETag.assemble(channel.event.id, channel.relay, channel.event.pubKey)), + ) = create( + channels = listOf(channel), isPrivate = isPrivate, signer = signer, createdAt = createdAt, - onReady = onReady, ) - fun createChannel( - channel: EventIdHintOptional, + suspend fun create( + channels: List, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ChannelListEvent) -> Unit, - ) = createChannelBase( - tags = arrayOf(ETag.assemble(channel.eventId, channel.relay, null)), + ): ChannelListEvent = + if (isPrivate) { + create( + publicChannels = emptyList(), + privateChannels = channels, + signer = signer, + createdAt = createdAt, + ) + } else { + create( + publicChannels = channels, + privateChannels = emptyList(), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun add( + earlierVersion: ChannelListEvent, + channel: ChannelTag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = add( + earlierVersion = earlierVersion, + channels = listOf(channel), isPrivate = isPrivate, signer = signer, createdAt = createdAt, - onReady = onReady, ) - fun createChannels( - channels: List, + suspend fun add( + earlierVersion: ChannelListEvent, + channels: List, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ChannelListEvent) -> Unit, - ) = createChannelBase( - tags = channels.map { ETag.assemble(it.eventId, it.relay, null) }.toTypedArray(), - isPrivate = isPrivate, + ): ChannelListEvent = + if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + resign( + tags = earlierVersion.tags, + privateTags = privateTags.removeAny(channels.map { it.toTagIdOnly() }) + channels.map { it.toTagArray() }, + signer = signer, + createdAt = createdAt, + ) + } else { + resign( + content = earlierVersion.content, + tags = earlierVersion.tags.removeAny(channels.map { it.toTagIdOnly() }) + channels.map { it.toTagArray() }, + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun remove( + earlierVersion: ChannelListEvent, + channel: ChannelTag, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): ChannelListEvent { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + return resign( + privateTags = privateTags.remove(channel.toTagArray()), + tags = earlierVersion.tags.remove(channel.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun resign( + tags: TagArray, + privateTags: TagArray, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = resign( + content = PrivateTagsInContent.encryptNip04(privateTags, signer), + tags = tags, signer = signer, createdAt = createdAt, - onReady = onReady, ) - fun removeChannel( - earlierVersion: ChannelListEvent, - channel: HexKey, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChannelListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.removeAll( - earlierVersion, - ETag.assemble(channel, null, null), - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - private fun addChannelBase( - earlierVersion: ChannelListEvent, - newTags: Array>, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChannelListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.addAll( - earlierVersion, - newTags, - isPrivate, - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - fun addChannel( - earlierVersion: ChannelListEvent, - channel: EventHintBundle, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChannelListEvent) -> Unit, - ) = addChannelBase( - earlierVersion, - arrayOf(ETag.assemble(channel.event.id, channel.relay, channel.event.pubKey)), - isPrivate, - signer, - createdAt, - onReady, - ) - - fun addChannel( - earlierVersion: ChannelListEvent, - channel: EventIdHintOptional, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChannelListEvent) -> Unit, - ) = addChannelBase( - earlierVersion, - arrayOf(ETag.assemble(channel.eventId, channel.relay, null)), - isPrivate, - signer, - createdAt, - onReady, - ) - - fun addChannels( - earlierVersion: ChannelListEvent, - channels: List, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (ChannelListEvent) -> Unit, - ) = addChannelBase( - earlierVersion, - channels.map { ETag.assemble(it.eventId, it.relay, null) }.toTypedArray(), - isPrivate, - signer, - createdAt, - onReady, - ) - - private fun create( + suspend fun resign( content: String, - tags: Array>, + tags: TagArray, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (ChannelListEvent) -> Unit, - ) { + ): ChannelListEvent { val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { + if (tags.fastAny(AltTag::match)) { tags } else { - tags + AltTag.Companion.assemble(ALT) + tags + AltTag.assemble(ALT) } - signer.sign(createdAt, KIND, newTags, content, onReady) + return signer.sign(createdAt, KIND, newTags, content) + } + + suspend fun create( + publicChannels: List = emptyList(), + privateChannels: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): ChannelListEvent { + val template = build(publicChannels, privateChannels, signer, createdAt) + return signer.sign(template) } fun create( - list: List, + publicChannels: List = emptyList(), + privateChannels: List = emptyList(), signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), - ): ChannelListEvent? { - val tags = list.map { ETag.assemble(it.eventId, it.relay, null) }.toTypedArray() - return signer.sign(createdAt, KIND, tags, "") + ): ChannelListEvent { + val privateTagArray = privateChannels.map { it.toTagArray() }.toTypedArray() + val publicTagArray = publicChannels.map { it.toTagArray() }.toTypedArray() + AltTag.assemble(ALT) + return signer.signNip51List(createdAt, KIND, publicTagArray, privateTagArray) + } + + suspend fun build( + publicChannels: List = emptyList(), + privateChannels: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip04(privateChannels.map { it.toTagArray() }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + channels(publicChannels) + + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt index 2c026c622e..ef99d88271 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayBuilderExt.kt @@ -24,8 +24,11 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag fun TagArrayBuilder.followChat( eventId: HexKey, relayUrl: NormalizedRelayUrl, ) = addUnique(ETag.assemble(eventId, relayUrl, null)) + +fun TagArrayBuilder.channels(rooms: List) = addAll(rooms.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayExt.kt new file mode 100644 index 0000000000..b0b929173e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/TagArrayExt.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip28PublicChat.list + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag + +fun TagArray.channels() = mapNotNull(ChannelTag::parse) + +fun TagArray.channelSet() = mapNotNullTo(mutableSetOf(), ChannelTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt new file mode 100644 index 0000000000..88aafdff84 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip28PublicChat.list.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.ensure +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +@Immutable +class ChannelTag( + val eventId: HexKey, + val relay: NormalizedRelayUrl? = null, + val author: HexKey? = null, +) { + fun countMemory(): Long = + 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) + eventId.bytesUsedInMemory() + + (relay?.url?.bytesUsedInMemory() ?: 0) + + (author?.bytesUsedInMemory() ?: 0) + + fun toNEvent(): String = NEvent.create(eventId, author, null, relay) + + fun toTagArray() = assemble(eventId, relay, author) + + fun toTagIdOnly() = assemble(eventId, null, null) + + companion object { + const val TAG_NAME = "e" + + @JvmStatic + fun isTagged(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64 + + @JvmStatic + fun isTagged( + tag: Array, + eventId: HexKey, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == eventId + + @JvmStatic + fun parse(tag: Array): ChannelTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + return ChannelTag(tag[1], pickRelayHint(tag), pickAuthor(tag)) + } + + @JvmStatic + fun parseId(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + return tag[1] + } + + @JvmStatic + private fun pickRelayHint(tag: Array): NormalizedRelayUrl? { + if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2]) + if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3]) + if (tag.has(4) && tag[4].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[4])) return RelayUrlNormalizer.normalizeOrNull(tag[4]) + return null + } + + @JvmStatic + private fun pickAuthor(tag: Array): HexKey? { + if (tag.has(2) && tag[2].length == 64) return tag[2] + if (tag.has(3) && tag[3].length == 64) return tag[3] + if (tag.has(4) && tag[4].length == 64) return tag[4] + return null + } + + @JvmStatic + fun parseAsHint(tag: Array): EventIdHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val hint = pickRelayHint(tag) + + ensure(hint != null) { return null } + + return EventIdHint(tag[1], hint) + } + + @JvmStatic + fun assemble( + eventId: HexKey, + relay: NormalizedRelayUrl?, + author: HexKey?, + ) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, author) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt index 4fdb1bbc7e..4e7f4d0459 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip30CustomEmoji/pack/EmojiPackEvent.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip34Git.repository.name -import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.utils.TimeUtils import java.util.UUID @@ -40,7 +40,7 @@ class EmojiPackEvent( tags: Array>, content: String, sig: HexKey, -) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { companion object { const val KIND = 30030 const val ALT_DESCRIPTION = "Emoji pack" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTag.kt index 66b44623e7..910c5a1793 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip31Alts/AltTag.kt @@ -27,6 +27,9 @@ class AltTag { companion object { const val TAG_NAME = "alt" + @JvmStatic + fun match(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + @JvmStatic fun parse(tag: Array): String? { ensure(tag.has(1)) { return null } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt index 367c85bc88..ab12e8be3d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/patch/GitPatchEvent.kt @@ -107,12 +107,11 @@ class GitPatchEvent( const val KIND = 1617 const val ALT = "A Git Patch" - fun create( + suspend fun create( patch: String, createdAt: Long = TimeUtils.now(), signer: NostrSigner, - onReady: (GitPatchEvent) -> Unit, - ) { + ): GitPatchEvent { val content = patch val tags = mutableListOf( @@ -121,7 +120,7 @@ class GitPatchEvent( tags.add(AltTag.assemble(ALT)) - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + return signer.sign(createdAt, KIND, tags.toTypedArray(), content) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt index 15e9a15c8f..9a6befef48 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftBuilder.kt @@ -30,30 +30,25 @@ import com.vitorpamplona.quartz.utils.TimeUtils class DraftBuilder { companion object { - fun encryptAndSign( + suspend fun encryptAndSign( dTag: String, draft: T, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { - signer.nip44Encrypt(draft.toJson(), signer.pubKey) { encryptedContent -> - val template = - eventTemplate(DraftEvent.KIND, encryptedContent, createdAt) { - alt(DraftEvent.ALT_DESCRIPTION) - dTag(dTag) - kind(draft.kind) + ): DraftEvent { + val encryptedContent = signer.nip44Encrypt(draft.toJson(), signer.pubKey) + val template = + eventTemplate(DraftEvent.KIND, encryptedContent, createdAt) { + alt(DraftEvent.ALT_DESCRIPTION) + dTag(dTag) + kind(draft.kind) - if (draft is ExposeInDraft) { - addAll(draft.exposeInDraft()) - } + if (draft is ExposeInDraft) { + addAll(draft.exposeInDraft()) } - - signer.sign(template) { - it.addToCache(signer.pubKey, draft) - onReady(it) } - } + + return signer.sign(template) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt index 7e25c3bb78..b982ca9fdd 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEvent.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.quartz.nip37Drafts +import android.util.Log import androidx.compose.runtime.Immutable +import com.fasterxml.jackson.core.JsonParseException import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent @@ -30,6 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.events.ETag @@ -41,7 +44,6 @@ import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class DraftEvent( @@ -67,76 +69,23 @@ class DraftEvent( override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) - @Transient private var cachedInnerEvent: Map = mapOf() - - override fun countMemory(): Long = - super.countMemory() + - 32 + (cachedInnerEvent.values.sumOf { pointerSizeInBytes + (it?.countMemory() ?: 0) }) - override fun isContentEncoded() = true fun isDeleted() = content == "" - fun preCachedDraft(signer: NostrSigner): Event? = cachedInnerEvent[signer.pubKey] + fun canDecrypt(signer: NostrSigner) = signer.pubKey == pubKey - fun preCachedDraft(pubKey: HexKey): Event? = cachedInnerEvent[pubKey] + suspend fun createDeletedEvent(signer: NostrSigner): DraftEvent = signer.sign(createdAt, KIND, tags, "") - fun allCache() = cachedInnerEvent.values + suspend fun decryptInnerEvent(signer: NostrSigner): Event { + if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException() - fun addToCache( - pubKey: HexKey, - innerEvent: Event, - ) { - cachedInnerEvent = cachedInnerEvent + Pair(pubKey, innerEvent) - } - - fun cachedDraft( - signer: NostrSigner, - onReady: (Event) -> Unit, - ) { - cachedInnerEvent[signer.pubKey]?.let { - onReady(it) - return - } - decrypt(signer) { draft -> - addToCache(signer.pubKey, draft) - - onReady(draft) - } - } - - private fun decrypt( - signer: NostrSigner, - onReady: (Event) -> Unit, - ) { - try { - plainContent(signer) { - try { - onReady(fromJson(it)) - } catch (e: Exception) { - // Log.e("UnwrapError", "Couldn't Decrypt the content", e) - } - } - } catch (e: Exception) { - // Log.e("UnwrapError", "Couldn't Decrypt the content", e) - } - } - - private fun plainContent( - signer: NostrSigner, - onReady: (String) -> Unit, - ) { - if (content.isEmpty()) return - - signer.nip44Decrypt(content, pubKey, onReady) - } - - fun createDeletedEvent( - signer: NostrSigner, - onReady: (DraftEvent) -> Unit, - ) { - signer.sign(createdAt, KIND, tags, "") { - onReady(it) + val json = signer.nip44Decrypt(content, pubKey) + return try { + fromJson(json) + } catch (e: JsonParseException) { + Log.w("DraftEvent", "Unable to parse inner event of a draft: $json") + throw e } } @@ -149,123 +98,116 @@ class DraftEvent( dTag: String, ): String = Address.assemble(KIND, pubKey, dTag) - fun create( + @Suppress("DEPRECATION") + suspend fun create( dTag: String, originalNote: TorrentCommentEvent, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { + ): DraftEvent { val tagsWithMarkers = originalNote.tags.filter { it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply") } - create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady) + return create(dTag, originalNote, tagsWithMarkers, signer, createdAt) } - fun create( + suspend fun create( dTag: String, originalNote: InteractiveStoryBaseEvent, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { + ): DraftEvent { val tags = mutableListOf>() - create(dTag, originalNote, tags, signer, createdAt, onReady) + return create(dTag, originalNote, tags, signer, createdAt) } - fun create( + suspend fun create( dTag: String, originalNote: LiveActivitiesChatMessageEvent, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { + ): DraftEvent { val tags = mutableListOf>() originalNote.activity()?.let { tags.add(arrayOf("a", it.toTag(), "", "root")) } originalNote.replyingTo()?.let { tags.add(arrayOf("e", it, "", "reply")) } - create(dTag, originalNote, tags, signer, createdAt, onReady) + return create(dTag, originalNote, tags, signer, createdAt) } - fun create( + suspend fun create( dTag: String, originalNote: ChannelMessageEvent, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { + ): DraftEvent { val tags = mutableListOf>() originalNote.channelId()?.let { tags.add(arrayOf("e", it)) } - create(dTag, originalNote, tags, signer, createdAt, onReady) + return create(dTag, originalNote, tags, signer, createdAt) } - fun create( + @Suppress("DEPRECATION") + suspend fun create( dTag: String, originalNote: GitReplyEvent, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { + ): DraftEvent { val tags = mutableListOf>() originalNote.repository()?.let { tags.add(arrayOf("a", it.toTag())) } originalNote.replyingTo()?.let { tags.add(arrayOf("e", it)) } - create(dTag, originalNote, tags, signer, createdAt, onReady) + return create(dTag, originalNote, tags, signer, createdAt) } - fun create( + suspend fun create( dTag: String, originalNote: PollNoteEvent, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { + ): DraftEvent { val tagsWithMarkers = originalNote.tags.filter { it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply") } - create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady) + return create(dTag, originalNote, tagsWithMarkers, signer, createdAt) } - fun create( + suspend fun create( dTag: String, originalNote: CommentEvent, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { + ): DraftEvent { val tagsWithMarkers = originalNote.rootScopes() + originalNote.directReplies() - create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady) + return create(dTag, originalNote, tagsWithMarkers, signer, createdAt) } - fun create( + suspend fun create( dTag: String, originalNote: TextNoteEvent, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { + ): DraftEvent { val tagsWithMarkers = originalNote.tags.filter { it.size > 3 && (it[0] == "e" || it[0] == "a") && (it[3] == "root" || it[3] == "reply") } - create(dTag, originalNote, tagsWithMarkers, signer, createdAt, onReady) + return create(dTag, originalNote, tagsWithMarkers, signer, createdAt) } - fun create( + suspend fun create( dTag: String, innerEvent: Event, anchorTagArray: List> = emptyList(), signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (DraftEvent) -> Unit, - ) { + ): DraftEvent { val tags = mutableListOf>() tags.add(arrayOf("d", dTag)) tags.add(arrayOf("k", "${innerEvent.kind}")) @@ -274,12 +216,15 @@ class DraftEvent( tags.addAll(anchorTagArray) } - signer.nip44Encrypt(innerEvent.toJson(), signer.pubKey) { encryptedContent -> - signer.sign(createdAt, KIND, tags.toTypedArray(), encryptedContent) { - it.addToCache(signer.pubKey, innerEvent) - onReady(it) - } - } + val draft = + signer.sign( + createdAt = createdAt, + kind = KIND, + tags = tags.toTypedArray(), + content = signer.nip44Encrypt(innerEvent.toJson(), signer.pubKey), + ) + + return draft } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEventCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEventCache.kt new file mode 100644 index 0000000000..ef14999b3e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/DraftEventCache.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip37Drafts + +import androidx.collection.LruCache +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache + +class DraftEventCache( + signer: NostrSigner, +) { + private val decryptionCache = + object : LruCache(1000) { + override fun create(key: DraftEvent): DraftEventDecryptCache? = + if (!key.isDeleted() && key.pubKey == signer.pubKey) { + DraftEventDecryptCache(signer) + } else { + null + } + } + + fun delete(event: DraftEvent) = decryptionCache.remove(event) + + fun preload( + event: DraftEvent, + result: Event, + ) = decryptionCache[event]?.preload(result) + + fun preCachedDraft(event: DraftEvent): Event? = decryptionCache[event]?.cached() + + suspend fun cachedDraft(event: DraftEvent) = decryptionCache[event]?.decrypt(event) +} + +class DraftEventDecryptCache( + signer: NostrSigner, +) : DecryptCache(signer) { + override suspend fun decryptAndParse( + event: DraftEvent, + signer: NostrSigner, + ): Event = event.decryptInnerEvent(signer) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/privateOutbox/PrivateOutboxRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/privateOutbox/PrivateOutboxRelayListEvent.kt new file mode 100644 index 0000000000..05a93432b7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip37Drafts/privateOutbox/PrivateOutboxRelayListEvent.kt @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip37Drafts.privateOutbox + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.privateRelays +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class PrivateOutboxRelayListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicRelays() = tags.mapNotNull(RelayTag::parse) + + suspend fun privateRelays(signer: NostrSigner) = privateTags(signer)?.mapNotNull(RelayTag::parse) + + suspend fun relays(signer: NostrSigner): List = publicRelays() + (privateRelays(signer) ?: emptyList()) + + companion object { + const val KIND = 10013 + const val FIXED_D_TAG = "" + + val ALT = "Relay list to store private content from this author" + val TAGS = arrayOf(AltTag.assemble(ALT)) + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) + + fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) + + suspend fun updateRelayList( + earlierVersion: PrivateOutboxRelayListEvent, + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): PrivateOutboxRelayListEvent { + val newRelayList = relays.map { RelayTag.assemble(it) } + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + + val publicTags = earlierVersion.tags.remove(RelayTag::match) + val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) + + return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags) + } + + suspend fun create( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): PrivateOutboxRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun create( + relays: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): PrivateOutboxRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun build( + publicRelays: List = emptyList(), + privateRelays: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + privateRelays(publicRelays) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt index 43eb9d1b0e..17aed53f05 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt @@ -41,42 +41,39 @@ class StatusEvent( companion object { const val KIND = 30315 - fun create( + suspend fun create( msg: String, type: String, expiration: Long?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (StatusEvent) -> Unit, - ) { + ): StatusEvent { val tags = mutableListOf>() tags.add(arrayOf("d", type)) expiration?.let { tags.add(arrayOf("expiration", it.toString())) } - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + return signer.sign(createdAt, KIND, tags.toTypedArray(), msg) } - fun update( + suspend fun update( event: StatusEvent, newStatus: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (StatusEvent) -> Unit, - ) { + ): StatusEvent { val tags = event.tags - signer.sign(createdAt, KIND, tags, newStatus, onReady) + return signer.sign(createdAt, KIND, tags, newStatus) } - fun clear( + suspend fun clear( event: StatusEvent, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (StatusEvent) -> Unit, - ) { + ): StatusEvent { val msg = "" val tags = event.tags.filter { it.size > 1 && it[0] == "d" } - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + return signer.sign(createdAt, KIND, tags.toTypedArray(), msg) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt index 83d8a17974..acd65b79ab 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip42RelayAuth/RelayAuthEvent.kt @@ -45,36 +45,34 @@ class RelayAuthEvent( companion object { const val KIND = 22242 - fun create( + suspend fun create( relay: NormalizedRelayUrl, challenge: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (RelayAuthEvent) -> Unit, - ) { + ): RelayAuthEvent { val content = "" val tags = arrayOf( RelayTag.assemble(relay), ChallengeTag.assemble(challenge), ) - signer.sign(createdAt, KIND, tags, content, onReady) + return signer.sign(createdAt, KIND, tags, content) } - fun create( + suspend fun create( relays: List, challenge: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (RelayAuthEvent) -> Unit, - ) { + ): RelayAuthEvent { val content = "" val tags = relays .map { RelayTag.assemble(it) } .plusElement(ChallengeTag.assemble(challenge)) .toTypedArray() - signer.sign(createdAt, KIND, tags, content, onReady) + return signer.sign(createdAt, KIND, tags, content) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt index 7627fe0e24..43a2b2909c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip44Encryption -import android.util.Log import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip04Dm.crypto.EncryptedInfo import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04 @@ -49,8 +48,8 @@ object Nip44 { payload: String, privateKey: ByteArray, pubKey: ByteArray, - ): String? { - if (payload.isEmpty()) return null + ): String { + require(payload.isNotBlank()) { "Payload must not be blank" } return if (payload[0] == '{') { decryptNIP44FromJackson(payload, privateKey, pubKey) } else { @@ -62,14 +61,13 @@ object Nip44 { json: String, privateKey: ByteArray, pubKey: ByteArray, - ): String? { + ): String { // Ignores if it is not a valid json val info = try { JsonMapper.mapper.readValue(json, EncryptedInfoString::class.java) } catch (e: Exception) { - Log.e("NIP44", "Unable to parse json $json") - return null + throw IllegalArgumentException("Unable to parse NIP-44 JSON: $json") } return when (info.v) { @@ -101,31 +99,25 @@ object Nip44 { v2.decrypt(encryptedInfo, privateKey, pubKey) } - else -> null + else -> throw IllegalArgumentException("Invalid or unsupported NIP-44 version code ${info.v}") } } private fun decryptNIP44FromBase64( - payload: String, + ciphertext: String, privateKey: ByteArray, pubKey: ByteArray, - ): String? { - if (payload.isEmpty()) return null + ): String { + require(ciphertext.isNotBlank()) { "ciphertext must not be blank" } // Ignores if it is not base64 - val byteArray = - try { - Base64.getDecoder().decode(payload) - } catch (e: Exception) { - Log.e("NIP44", "Unable to parse base64 $payload") - return null - } + val byteArray = Base64.getDecoder().decode(ciphertext) return when (byteArray[0].toInt()) { - EncryptedInfo.V -> Nip04.decrypt(payload, privateKey, pubKey) - Nip44v1.EncryptedInfo.V -> v1.decrypt(payload, privateKey, pubKey) - Nip44v2.EncryptedInfo.V -> v2.decrypt(payload, privateKey, pubKey) - else -> null + EncryptedInfo.V -> Nip04.decrypt(ciphertext, privateKey, pubKey) + Nip44v1.EncryptedInfo.V -> v1.decrypt(ciphertext, privateKey, pubKey) + Nip44v2.EncryptedInfo.V -> v2.decrypt(ciphertext, privateKey, pubKey) + else -> throw IllegalArgumentException("Invalid or unsupported NIP-44 version code ${byteArray[0].toInt()}") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt index 942ecf47c0..d7f6824722 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v1.kt @@ -20,11 +20,11 @@ */ package com.vitorpamplona.quartz.nip44Encryption -import android.util.Log import com.vitorpamplona.quartz.utils.LibSodiumInstance import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.Secp256k1Instance import com.vitorpamplona.quartz.utils.sha256.sha256 +import kotlinx.coroutines.CancellationException import java.util.Base64 class Nip44v1 { @@ -57,7 +57,7 @@ class Nip44v1 { ) return EncryptedInfo( - ciphertext = cipher ?: ByteArray(0), + ciphertext = cipher, nonce = nonce, ) } @@ -66,7 +66,7 @@ class Nip44v1 { payload: String, privateKey: ByteArray, pubKey: ByteArray, - ): String? { + ): String { val sharedSecret = getSharedSecret(privateKey, pubKey) return decrypt(payload, sharedSecret) } @@ -75,7 +75,7 @@ class Nip44v1 { encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray, - ): String? { + ): String { val sharedSecret = getSharedSecret(privateKey, pubKey) return decrypt(encryptedInfo, sharedSecret) } @@ -83,21 +83,21 @@ class Nip44v1 { fun decrypt( payload: String, sharedSecret: ByteArray, - ): String? { - val encryptedInfo = EncryptedInfo.decodePayload(payload) ?: return null + ): String { + val encryptedInfo = EncryptedInfo.decodePayload(payload) return decrypt(encryptedInfo, sharedSecret) } fun decrypt( encryptedInfo: EncryptedInfo, sharedSecret: ByteArray, - ): String? = + ): String = LibSodiumInstance .cryptoStreamXChaCha20Xor( messageBytes = encryptedInfo.ciphertext, nonce = encryptedInfo.nonce, key = sharedSecret, - )?.decodeToString() + ).decodeToString() fun getSharedSecret( privateKey: ByteArray, @@ -127,19 +127,18 @@ class Nip44v1 { companion object { const val V: Int = 1 - fun decodePayload(payload: String): EncryptedInfo? { - return try { + fun decodePayload(payload: String): EncryptedInfo = + try { val byteArray = Base64.getDecoder().decode(payload) check(byteArray[0].toInt() == V) - return EncryptedInfo( + EncryptedInfo( nonce = byteArray.copyOfRange(1, 25), ciphertext = byteArray.copyOfRange(25, byteArray.size), ) } catch (e: Exception) { - Log.w("NIP44v1", "Unable to Parse encrypted payload: $payload") - null + if (e is CancellationException) throw e + throw IllegalStateException("NIP-44v1 Unable to Parse encrypted payload: $payload", e) } - } } fun encodePayload(): String = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt index 594658ac6c..e0ca8c2bd0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.quartz.nip44Encryption -import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip44Encryption.crypto.Hkdf import com.vitorpamplona.quartz.utils.LibSodiumInstance import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.Secp256k1Instance +import kotlinx.coroutines.CancellationException import java.nio.ByteBuffer import java.nio.ByteOrder import java.util.Base64 @@ -88,26 +88,26 @@ class Nip44v2 { payload: String, privateKey: ByteArray, pubKey: ByteArray, - ): String? = decrypt(payload, getConversationKey(privateKey, pubKey)) + ): String = decrypt(payload, getConversationKey(privateKey, pubKey)) fun decrypt( decoded: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray, - ): String? = decrypt(decoded, getConversationKey(privateKey, pubKey)) + ): String = decrypt(decoded, getConversationKey(privateKey, pubKey)) fun decrypt( payload: String, conversationKey: ByteArray, - ): String? { - val decoded = EncryptedInfo.decodePayload(payload) ?: return null + ): String { + val decoded = EncryptedInfo.decodePayload(payload) return decrypt(decoded, conversationKey) } fun decrypt( decoded: EncryptedInfo, conversationKey: ByteArray, - ): String? { + ): String { val messageKey = getMessageKeys(conversationKey, decoded.nonce) val calculatedMac = hmacAad(messageKey.hmacKey, decoded.ciphertext, decoded.nonce) @@ -236,7 +236,7 @@ class Nip44v2 { companion object { const val V: Int = 2 - fun decodePayload(payload: String): EncryptedInfo? { + fun decodePayload(payload: String): EncryptedInfo { check(payload.length >= 132 || payload.length <= 87472) { "Invalid payload length ${payload.length} for $payload" } @@ -245,14 +245,14 @@ class Nip44v2 { return try { val byteArray = Base64.getDecoder().decode(payload) check(byteArray[0].toInt() == V) - return EncryptedInfo( + EncryptedInfo( nonce = byteArray.copyOfRange(1, 33), ciphertext = byteArray.copyOfRange(33, byteArray.size - 32), mac = byteArray.copyOfRange(byteArray.size - 32, byteArray.size), ) } catch (e: Exception) { - Log.w("NIP44v2", "Unable to Parse encrypted payload: $payload") - null + if (e is CancellationException) throw e + throw IllegalStateException("NIP-44v2 Unable to Parse encrypted payload: $payload", e) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt index 231c88a10a..cc07e06850 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/NostrConnectEvent.kt @@ -25,10 +25,10 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class NostrConnectEvent( @@ -39,17 +39,18 @@ class NostrConnectEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient private var decryptedContent: Map = mapOf() - - override fun countMemory(): Long = - super.countMemory() + - pointerSizeInBytes + (decryptedContent.values.sumOf { pointerSizeInBytes + it.countMemory() }) - override fun isContentEncoded() = true - private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) + fun canDecrypt(signer: NostrSigner) = pubKey == signer.pubKey || recipientPubKey() == signer.pubKey - fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this) }?.getOrNull() + suspend fun decryptMessage(signer: NostrSigner): BunkerMessage { + if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException() + + val retVal = signer.decrypt(content, talkingWith(signer.pubKey)) + return JsonMapper.mapper.readValue(retVal, BunkerMessage::class.java) + } + + private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) fun verifiedRecipientPubKey(): HexKey? { val recipient = recipientPubKey() @@ -62,47 +63,29 @@ class NostrConnectEvent( fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) verifiedRecipientPubKey() ?: pubKey else pubKey - fun plainContent( - signer: NostrSigner, - onReady: (BunkerMessage) -> Unit, - ) { - decryptedContent[signer.pubKey]?.let { - onReady(it) - return - } - - // decrypts using NIP-04 or NIP-44 - signer.decrypt(content, talkingWith(signer.pubKey)) { retVal -> - val content = JsonMapper.mapper.readValue(retVal, BunkerMessage::class.java) - - decryptedContent = decryptedContent + Pair(signer.pubKey, content) - - onReady(content) - } - } - companion object { const val KIND = 24133 const val ALT = "Nostr Connect Event" - fun create( + suspend fun create( message: BunkerMessage, remoteKey: HexKey, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (NostrConnectEvent) -> Unit, - ) { - val tags = - arrayOf( - AltTag.assemble(ALT), - arrayOf("p", remoteKey), - ) - - val encrypted = JsonMapper.mapper.writeValueAsString(message) - - signer.nip44Encrypt(encrypted, remoteKey) { content -> - signer.sign(createdAt, KIND, tags, content, onReady) - } - } + ): NostrConnectEvent = + signer.sign( + createdAt = createdAt, + kind = KIND, + tags = + arrayOf( + AltTag.assemble(ALT), + arrayOf("p", remoteKey), + ), + content = + signer.nip44Encrypt( + plaintext = JsonMapper.mapper.writeValueAsString(message), + toPublicKey = remoteKey, + ), + ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt index 884f6aa90d..919bc33a89 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt @@ -20,16 +20,14 @@ */ package com.vitorpamplona.quartz.nip47WalletConnect -import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class LnZapPaymentRequestEvent( @@ -40,60 +38,41 @@ class LnZapPaymentRequestEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - // Once one of an app user decrypts the payment, all users else can see it. - @Transient private var lnInvoice: String? = null - - override fun countMemory(): Long = - super.countMemory() + - pointerSizeInBytes + (lnInvoice?.bytesUsedInMemory() ?: 0) // rough calculation + override fun isContentEncoded() = true fun walletServicePubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) walletServicePubKey() ?: pubKey else pubKey - fun lnInvoice( - signer: NostrSigner, - onReady: (String) -> Unit, - ) { - lnInvoice?.let { - onReady(it) - return - } + fun canDecrypt(signer: NostrSigner) = pubKey == signer.pubKey || walletServicePubKey() == signer.pubKey - try { - signer.decrypt(content, talkingWith(signer.pubKey)) { jsonText -> - val payInvoiceMethod = JsonMapper.mapper.readValue(jsonText, Request::class.java) - - lnInvoice = (payInvoiceMethod as? PayInvoiceMethod)?.params?.invoice - - lnInvoice?.let { onReady(it) } - } - } catch (e: Exception) { - Log.w("BookmarkList", "Error decrypting the message ${e.message}") - } + suspend fun decryptRequest(signer: NostrSigner): Request { + if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException() + val jsonText = signer.decrypt(content, talkingWith(signer.pubKey)) + return JsonMapper.mapper.readValue(jsonText, Request::class.java) } companion object { const val KIND = 23194 const val ALT = "Zap payment request" - fun create( + suspend fun create( lnInvoice: String, walletServicePubkey: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (LnZapPaymentRequestEvent) -> Unit, - ) { + ): LnZapPaymentRequestEvent { val serializedRequest = JsonMapper.mapper.writeValueAsString(PayInvoiceMethod.create(lnInvoice)) val tags = arrayOf(arrayOf("p", walletServicePubkey), AltTag.assemble(ALT)) - signer.nip04Encrypt( - serializedRequest, - walletServicePubkey, - ) { content -> - signer.sign(createdAt, KIND, tags, content, onReady) - } + val encrypted = + signer.nip04Encrypt( + serializedRequest, + walletServicePubkey, + ) + + return signer.sign(createdAt, KIND, tags, encrypted) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt index c065a2a27c..1f019bac67 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt @@ -20,13 +20,12 @@ */ package com.vitorpamplona.quartz.nip47WalletConnect -import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.utils.pointerSizeInBytes +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions @Immutable class LnZapPaymentResponseEvent( @@ -37,10 +36,7 @@ class LnZapPaymentResponseEvent( content: String, sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { - // Once one of an app user decrypts the payment, all users else can see it. - @Transient private var response: Response? = null - - override fun countMemory(): Long = super.countMemory() + pointerSizeInBytes + (response?.countMemory() ?: 0) + override fun isContentEncoded() = true fun requestAuthor() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) @@ -48,38 +44,13 @@ class LnZapPaymentResponseEvent( fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) requestAuthor() ?: pubKey else pubKey - private fun plainContent( - signer: NostrSigner, - onReady: (String) -> Unit, - ) { - try { - signer.decrypt(content, talkingWith(signer.pubKey)) { content -> onReady(content) } - } catch (e: Exception) { - Log.w("PrivateDM", "Error decrypting the message ${e.message}") - } - } + fun canDecrypt(signer: NostrSigner) = pubKey == signer.pubKey || requestAuthor() == signer.pubKey - fun response( - signer: NostrSigner, - onReady: (Response) -> Unit, - ) { - response?.let { - onReady(it) - return - } + suspend fun decrypt(signer: NostrSigner): Response { + if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException() - try { - if (content.isNotEmpty()) { - plainContent(signer) { - JsonMapper.mapper.readValue(it, Response::class.java)?.let { - response = it - onReady(it) - } - } - } - } catch (e: Exception) { - Log.w("LnZapPaymentResponseEvent", "Can't parse content as a payment response: $content", e) - } + val json = signer.decrypt(content, talkingWith(signer.pubKey)) + return JsonMapper.mapper.readValue(json, Response::class.java) } companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt index 5a755f6d6a..22c696dac1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt @@ -63,7 +63,7 @@ class Nip47WalletConnect { val relayUri: String, val secret: HexKey?, ) { - fun normalize(): Nip47WalletConnect.Nip47URINorm? = + fun normalize(): Nip47URINorm? = RelayUrlNormalizer.normalizeOrNull(relayUri)?.let { Nip47URINorm( pubKeyHex, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/NostrWalletConnectRequestCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/NostrWalletConnectRequestCache.kt new file mode 100644 index 0000000000..890fce7ac5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/NostrWalletConnectRequestCache.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import androidx.collection.LruCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache + +class NostrWalletConnectRequestCache( + signer: NostrSigner, +) { + private val decryptionCache = + object : LruCache(50) { + override fun create(key: LnZapPaymentRequestEvent): NWCRequestDecryptCache? = + if (key.content.isNotBlank() && key.canDecrypt(signer)) { + NWCRequestDecryptCache(signer) + } else { + null + } + } + + fun cachedRequest(event: LnZapPaymentRequestEvent): Request? = decryptionCache[event]?.cached() + + suspend fun decryptRequest(event: LnZapPaymentRequestEvent) = decryptionCache[event]?.decrypt(event) +} + +class NWCRequestDecryptCache( + signer: NostrSigner, +) : DecryptCache(signer) { + override suspend fun decryptAndParse( + event: LnZapPaymentRequestEvent, + signer: NostrSigner, + ) = event.decryptRequest(signer) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/NostrWalletConnectResponseCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/NostrWalletConnectResponseCache.kt new file mode 100644 index 0000000000..d4b31fb67f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/NostrWalletConnectResponseCache.kt @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import androidx.collection.LruCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache + +class NostrWalletConnectResponseCache( + signer: NostrSigner, +) { + private val decryptionCache = + object : LruCache(50) { + override fun create(key: LnZapPaymentResponseEvent): NWCResponseDecryptCache? = + if (key.content.isNotBlank() && key.canDecrypt(signer)) { + NWCResponseDecryptCache(signer) + } else { + null + } + } + + fun cachedResponse(event: LnZapPaymentResponseEvent): Response? = decryptionCache[event]?.cached() + + suspend fun decryptResponse(event: LnZapPaymentResponseEvent) = decryptionCache[event]?.decrypt(event) +} + +class NWCResponseDecryptCache( + signer: NostrSigner, +) : DecryptCache(signer) { + override suspend fun decryptAndParse( + event: LnZapPaymentResponseEvent, + signer: NostrSigner, + ) = event.decrypt(signer) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt index 8ef77d9f48..6dd52abc33 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchRelayListEvent.kt @@ -21,16 +21,27 @@ package com.vitorpamplona.quartz.nip50Search import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip50Search.tags.RelayTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relays +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.searchRelays +import com.vitorpamplona.quartz.nip51Lists.remove import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.plus +import kotlin.collections.toTypedArray @Immutable class SearchRelayListEvent( @@ -40,66 +51,72 @@ class SearchRelayListEvent( tags: Array>, content: String, sig: HexKey, -) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun relays(): List = tags.mapNotNull(RelayTag::parse) +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicRelays() = tags.relays() + + suspend fun privateRelays(signer: NostrSigner) = privateTags(signer)?.relays() + + suspend fun relays(signer: NostrSigner): List = publicRelays() + (privateRelays(signer) ?: emptyList()) companion object { const val KIND = 10007 + val ALT = "Relay list to use for Search" + val ALT_TAG = arrayOf(AltTag.assemble(ALT)) - fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey) - fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey) - fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) + fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey) - fun createTagArray(relays: List): Array> = - relays - .map { - RelayTag.assemble(it) - }.plusElement(AltTag.assemble("Relay list to use for Search")) - .toTypedArray() - - fun updateRelayList( + suspend fun updateRelayList( earlierVersion: SearchRelayListEvent, relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (SearchRelayListEvent) -> Unit, - ) { - val tags = - earlierVersion.tags - .filter(RelayTag::notMatch) - .plus( - relays.map { - RelayTag.assemble(it) - }, - ).toTypedArray() + ): SearchRelayListEvent { + val newRelayList = relays.map { RelayTag.assemble(it) } + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() - signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady) + val publicTags = earlierVersion.tags.remove(RelayTag::match) + val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) + + return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags) } - fun createFromScratch( + suspend fun create( relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (SearchRelayListEvent) -> Unit, - ) { - create(relays, signer, createdAt, onReady) - } - - fun create( - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (SearchRelayListEvent) -> Unit, - ) { - signer.sign(createdAt, KIND, createTagArray(relays), "", onReady) + ): SearchRelayListEvent { + val publicTagArray = relays.map { RelayTag.assemble(it) }.plus(ALT_TAG).toTypedArray() + return signer.signNip51List(createdAt, KIND, publicTagArray, emptyArray()) } fun create( relays: List, signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), - ): SearchRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "") + ): SearchRelayListEvent { + val publicTagArray = relays.map { RelayTag.assemble(it) }.plus(ALT_TAG).toTypedArray() + return signer.signNip51List(createdAt, KIND, publicTagArray, emptyArray()) + } + + suspend fun build( + publicRelays: List = emptyList(), + privateRelays: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + searchRelays(publicRelays) + + initializer() + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BlockedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BlockedRelayListEvent.kt deleted file mode 100644 index 12c371db2e..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BlockedRelayListEvent.kt +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip51Lists - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip50Search.tags.RelayTag -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class BlockedRelayListEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun relays(): List = tags.mapNotNull(RelayTag::parse) - - companion object { - const val KIND = 10006 - - fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) - - fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) - - fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) - - fun createTagArray(relays: List): Array> = - relays - .map { - RelayTag.assemble(it) - }.plusElement(AltTag.assemble("Relay list to use for trusted connections")) - .toTypedArray() - - fun updateRelayList( - earlierVersion: BlockedRelayListEvent, - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BlockedRelayListEvent) -> Unit, - ) { - val tags = - earlierVersion.tags - .filter(RelayTag::notMatch) - .plus( - relays.map { - RelayTag.assemble(it) - }, - ).toTypedArray() - - signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady) - } - - fun createFromScratch( - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BlockedRelayListEvent) -> Unit, - ) { - create(relays, signer, createdAt, onReady) - } - - fun create( - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BlockedRelayListEvent) -> Unit, - ) { - signer.sign(createdAt, KIND, createTagArray(relays), "", onReady) - } - - fun create( - relays: List, - signer: NostrSignerSync, - createdAt: Long = TimeUtils.now(), - ): BlockedRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "") - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt deleted file mode 100644 index ee43e98ec0..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt +++ /dev/null @@ -1,237 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip51Lists - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class BookmarkListEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun countBookmarks() = tags.count(ETag::isTagged) + tags.count(ATag::isTagged) - - companion object { - const val KIND = 30001 - const val ALT = "List of bookmarks" - const val DEFAULT_D_TAG_BOOKMARKS = "bookmark" - - fun addEvent( - earlierVersion: BookmarkListEvent?, - eventId: HexKey, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BookmarkListEvent) -> Unit, - ) = addTag(earlierVersion, "e", eventId, isPrivate, signer, createdAt, onReady) - - fun addReplaceable( - earlierVersion: BookmarkListEvent?, - aTag: ATag, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BookmarkListEvent) -> Unit, - ) = addTag(earlierVersion, "a", aTag.toTag(), isPrivate, signer, createdAt, onReady) - - fun addTag( - earlierVersion: BookmarkListEvent?, - tagName: String, - tagValue: HexKey, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BookmarkListEvent) -> Unit, - ) { - add( - earlierVersion, - arrayOf(arrayOf(tagName, tagValue)), - isPrivate, - signer, - createdAt, - onReady, - ) - } - - fun add( - earlierVersion: BookmarkListEvent?, - listNewTags: Array>, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BookmarkListEvent) -> Unit, - ) { - if (isPrivate) { - if (earlierVersion != null) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = privateTags.plus(listNewTags), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = earlierVersion.tags, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - encryptTags( - privateTags = listNewTags, - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = arrayOf(arrayOf("d", DEFAULT_D_TAG_BOOKMARKS)), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion?.content ?: "", - tags = (earlierVersion?.tags ?: arrayOf(arrayOf("d", DEFAULT_D_TAG_BOOKMARKS))).plus(listNewTags), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - - fun removeEvent( - earlierVersion: BookmarkListEvent, - eventId: HexKey, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BookmarkListEvent) -> Unit, - ) = removeTag(earlierVersion, "e", eventId, isPrivate, signer, createdAt, onReady) - - fun removeReplaceable( - earlierVersion: BookmarkListEvent, - aTag: ATag, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BookmarkListEvent) -> Unit, - ) = removeTag(earlierVersion, "a", aTag.toTag(), isPrivate, signer, createdAt, onReady) - - private fun removeTag( - earlierVersion: BookmarkListEvent, - tagName: String, - tagValue: HexKey, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BookmarkListEvent) -> Unit, - ) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = - privateTags - .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } - .toTypedArray(), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = - earlierVersion.tags - .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = - earlierVersion.tags - .filter { it.size <= 1 || !(it[0] == tagName && it[1] == tagValue) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - - fun create( - content: String, - tags: Array>, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BookmarkListEvent) -> Unit, - ) { - val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { - tags - } else { - tags + AltTag.assemble(ALT) - } - - signer.sign(createdAt, KIND, newTags, content, onReady) - } - - fun create( - name: String = "", - events: List? = null, - users: List? = null, - addresses: List? = null, - privEvents: List? = null, - privUsers: List? = null, - privAddresses: List? = null, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (BookmarkListEvent) -> Unit, - ) { - val tags = mutableListOf>() - tags.add(arrayOf("d", name)) - - events?.forEach { tags.add(arrayOf("e", it)) } - users?.forEach { tags.add(arrayOf("p", it)) } - addresses?.forEach { tags.add(arrayOf("a", it.toTag())) } - tags.add(AltTag.assemble(ALT)) - - createPrivateTags(privEvents, privUsers, privAddresses, signer) { content -> - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) - } - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt deleted file mode 100644 index 95dac426cf..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/GeneralListEvent.kt +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip51Lists - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.isTagged -import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider -import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider -import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider -import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag -import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag -import com.vitorpamplona.quartz.nip51Lists.tags.NameTag -import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag -import kotlinx.collections.immutable.ImmutableSet -import kotlinx.collections.immutable.toImmutableSet - -@Immutable -abstract class GeneralListEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - kind: Int, - tags: Array>, - content: String, - sig: HexKey, -) : PrivateTagArrayEvent(id, pubKey, createdAt, kind, tags, content, sig), - EventHintProvider, - AddressHintProvider, - PubKeyHintProvider { - override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(ETag::parseAsHint) ?: emptyList()) - - override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + (cachedPrivateTags()?.mapNotNull(ETag::parseId) ?: emptyList()) - - override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(ATag::parseAsHint) ?: emptyList()) - - override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + (cachedPrivateTags()?.mapNotNull(ATag::parseAddressId) ?: emptyList()) - - override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + (cachedPrivateTags()?.mapNotNull(PTag::parseAsHint) ?: emptyList()) - - override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + (cachedPrivateTags()?.mapNotNull(PTag::parseKey) ?: emptyList()) - - fun name() = tags.firstNotNullOfOrNull(NameTag::parse) - - @Deprecated("NIP-51 has deprecated Title. Use name instead", ReplaceWith("name()")) - fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - - fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse) - - fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - - fun nameOrTitle() = name() ?: title() - - fun filterTagList( - key: String, - privateTags: Array>?, - ): ImmutableSet { - val result = HashSet(tags.size + (privateTags?.size ?: 0)) - - privateTags?.let { it.filter { it.size > 1 && it[0] == key }.mapTo(result) { it[1] } } - - tags.filter { it.size > 1 && it[0] == key }.mapTo(result) { it[1] } - - return result.toImmutableSet() - } - - fun isTagged( - key: String, - tag: String, - isPrivate: Boolean, - signer: NostrSigner, - onReady: (Boolean) -> Unit, - ) = if (isPrivate) { - privateTagsOrEmpty(signer = signer) { - onReady( - it.any { it.size > 1 && it[0] == key && it[1] == tag }, - ) - } - } else { - onReady(tags.isTagged(key, tag)) - } - - fun privateTagsOrEmpty( - signer: NostrSigner, - onReady: (Array>) -> Unit, - ) { - privateTags(signer, onReady) - } - - fun privateTaggedUsers( - signer: NostrSigner, - onReady: (List) -> Unit, - ) = privateTags(signer) { onReady(filterUsers(it)) } - - fun privateHashtags( - signer: NostrSigner, - onReady: (List) -> Unit, - ) = privateTags(signer) { onReady(filterHashtags(it)) } - - fun privateGeohashes( - signer: NostrSigner, - onReady: (List) -> Unit, - ) = privateTags(signer) { onReady(filterGeohashes(it)) } - - fun privateTaggedEvents( - signer: NostrSigner, - onReady: (List) -> Unit, - ) = privateTags(signer) { onReady(filterEvents(it)) } - - fun privateATags( - signer: NostrSigner, - onReady: (List) -> Unit, - ) = privateTags(signer) { onReady(filterATags(it)) } - - fun privateAddress( - signer: NostrSigner, - onReady: (List
) -> Unit, - ) = privateTags(signer) { onReady(filterAddresses(it)) } - - fun filterUsers(tags: Array>): List = tags.mapNotNull(PTag::parseKey) - - fun filterHashtags(tags: Array>): List = tags.mapNotNull(HashtagTag::parse) - - fun filterGeohashes(tags: Array>): List = tags.geohashes() - - fun filterEvents(tags: Array>): List = tags.mapNotNull(ETag::parseId) - - fun filterATags(tags: Array>): List = tags.mapNotNull(ATag::parse) - - fun filterAddresses(tags: Array>): List
= tags.mapNotNull(ATag::parseAddress) - - companion object { - fun createPrivateTags( - privEvents: List? = null, - privUsers: List? = null, - privAddresses: List? = null, - signer: NostrSigner, - onReady: (String) -> Unit, - ) { - val privTags = mutableListOf>() - privEvents?.forEach { privTags.add(arrayOf("e", it)) } - privUsers?.forEach { privTags.add(arrayOf("p", it)) } - privAddresses?.forEach { privTags.add(arrayOf("a", it.toTag())) } - - return encryptTags(privTags.toTypedArray(), signer, onReady) - } - - fun encryptTags( - privateTags: Array>? = null, - signer: NostrSigner, - onReady: (String) -> Unit, - ) { - val msg = JsonMapper.mapper.writeValueAsString(privateTags) - - signer.nip04Encrypt( - msg, - signer.pubKey, - onReady, - ) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt deleted file mode 100644 index fc0eb9f1a1..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/MuteListEvent.kt +++ /dev/null @@ -1,285 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip51Lists - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent.UsersAndWords -import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.tryAndWait -import kotlin.coroutines.resume - -@Immutable -class MuteListEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - override fun dTag() = FIXED_D_TAG - - fun publicAndCachedUsersAndWords() = - UsersAndWords( - filterTagList("p", cachedPrivateTags()), - filterTagList("word", cachedPrivateTags()), - ) - - fun publicAndPrivateUsersAndWords( - signer: NostrSigner, - onReady: (UsersAndWords) -> Unit, - ) { - privateTagsOrEmpty(signer) { - onReady( - UsersAndWords( - filterTagList("p", it), - filterTagList("word", it), - ), - ) - } - } - - suspend fun publicAndPrivateUsersAndWords(signer: NostrSigner): UsersAndWords? = - tryAndWait { continuation -> - publicAndPrivateUsersAndWords(signer) { privateTagList -> - continuation.resume(privateTagList) - } - } - - companion object { - const val KIND = 10000 - const val FIXED_D_TAG = "" - const val ALT = "Mute List" - - fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) - - fun blockListFor(pubKeyHex: HexKey): String = "10000:$pubKeyHex:" - - fun createListWithTag( - key: String, - tag: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) { - if (isPrivate) { - encryptTags(arrayOf(arrayOf(key, tag)), signer) { encryptedTags -> - create( - content = encryptedTags, - tags = emptyArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } else { - create( - content = "", - tags = arrayOf(arrayOf(key, tag)), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - - fun createListWithUser( - pubKeyHex: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) = createListWithTag("p", pubKeyHex, isPrivate, signer, createdAt, onReady) - - fun createListWithWord( - word: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) = createListWithTag("word", word, isPrivate, signer, createdAt, onReady) - - fun addUsers( - earlierVersion: MuteListEvent, - listPubKeyHex: List, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = - privateTags.plus( - listPubKeyHex.map { arrayOf("p", it) }, - ), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = earlierVersion.tags, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = - earlierVersion.tags.plus( - listPubKeyHex.map { arrayOf("p", it) }, - ), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - - fun addWord( - earlierVersion: MuteListEvent, - word: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) = addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) - - fun addUser( - earlierVersion: MuteListEvent, - pubKeyHex: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) = addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) - - fun addTag( - earlierVersion: MuteListEvent, - key: String, - tag: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) { - earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> - if (!isTagged) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = privateTags.plus(element = arrayOf(key, tag)), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = earlierVersion.tags, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = earlierVersion.tags.plus(element = arrayOf(key, tag)), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } - } - - fun removeWord( - earlierVersion: MuteListEvent, - word: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) = removeTag(earlierVersion, "word", word, signer, createdAt, onReady) - - fun removeUser( - earlierVersion: MuteListEvent, - pubKeyHex: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) = removeTag(earlierVersion, "p", pubKeyHex, signer, createdAt, onReady) - - fun removeTag( - earlierVersion: MuteListEvent, - key: String, - tag: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = - privateTags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = - earlierVersion.tags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } - - fun create( - content: String, - tags: Array>, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (MuteListEvent) -> Unit, - ) { - val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { - tags - } else { - tags + AltTag.assemble(ALT) - } - - signer.sign(createdAt, KIND, newTags, content, onReady) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt deleted file mode 100644 index 762660a8e2..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PeopleListEvent.kt +++ /dev/null @@ -1,305 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip51Lists - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.tryAndWait -import kotlin.coroutines.resume - -@Immutable -class PeopleListEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Immutable - class UsersAndWords( - val users: Set = setOf(), - val words: Set = setOf(), - ) - - fun publicAndCachedPrivateUsersAndWords() = - UsersAndWords( - filterTagList("p", cachedPrivateTags()), - filterTagList("word", cachedPrivateTags()), - ) - - fun publicAndPrivateUsersAndWords( - signer: NostrSigner, - onReady: (UsersAndWords) -> Unit, - ) { - privateTagsOrEmpty(signer) { - onReady( - UsersAndWords( - filterTagList("p", it), - filterTagList("word", it), - ), - ) - } - } - - suspend fun publicAndPrivateUsersAndWords(signer: NostrSigner): UsersAndWords? = - tryAndWait { continuation -> - publicAndPrivateUsersAndWords(signer) { privateTagList -> - continuation.resume(privateTagList) - } - } - - fun isTaggedWord( - word: String, - isPrivate: Boolean, - signer: NostrSigner, - onReady: (Boolean) -> Unit, - ) = isTagged("word", word, isPrivate, signer, onReady) - - fun isTaggedUser( - idHex: String, - isPrivate: Boolean, - signer: NostrSigner, - onReady: (Boolean) -> Unit, - ) = isTagged("p", idHex, isPrivate, signer, onReady) - - companion object { - const val KIND = 30000 - const val BLOCK_LIST_D_TAG = "mute" - const val ALT = "List of people" - - fun createBlockAddress(pubKey: HexKey) = Address(KIND, pubKey, BLOCK_LIST_D_TAG) - - fun blockListFor(pubKeyHex: HexKey): String = "30000:$pubKeyHex:$BLOCK_LIST_D_TAG" - - fun createListWithTag( - name: String, - key: String, - tag: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) { - if (isPrivate) { - encryptTags(arrayOf(arrayOf(key, tag)), signer) { encryptedTags -> - create( - content = encryptedTags, - tags = arrayOf(arrayOf("d", name)), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } else { - create( - content = "", - tags = arrayOf(arrayOf("d", name), arrayOf(key, tag)), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - - fun createListWithUser( - name: String, - pubKeyHex: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) = createListWithTag(name, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) - - fun createListWithWord( - name: String, - word: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) = createListWithTag(name, "word", word, isPrivate, signer, createdAt, onReady) - - fun addUsers( - earlierVersion: PeopleListEvent, - listPubKeyHex: List, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = - privateTags.plus( - listPubKeyHex.map { arrayOf("p", it) }, - ), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = earlierVersion.tags, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = - earlierVersion.tags.plus( - listPubKeyHex.map { arrayOf("p", it) }, - ), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - - fun addWord( - earlierVersion: PeopleListEvent, - word: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) = addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) - - fun addUser( - earlierVersion: PeopleListEvent, - pubKeyHex: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) = addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) - - fun addTag( - earlierVersion: PeopleListEvent, - key: String, - tag: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) { - earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> - if (!isTagged) { - if (isPrivate) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = privateTags.plus(element = arrayOf(key, tag)), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = earlierVersion.tags, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } else { - create( - content = earlierVersion.content, - tags = earlierVersion.tags.plus(element = arrayOf(key, tag)), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } - } - - fun removeWord( - earlierVersion: PeopleListEvent, - word: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) = removeTag(earlierVersion, "word", word, signer, createdAt, onReady) - - fun removeUser( - earlierVersion: PeopleListEvent, - pubKeyHex: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) = removeTag(earlierVersion, "p", pubKeyHex, signer, createdAt, onReady) - - fun removeTag( - earlierVersion: PeopleListEvent, - key: String, - tag: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) { - earlierVersion.privateTagsOrEmpty(signer) { privateTags -> - encryptTags( - privateTags = - privateTags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - ) { encryptedTags -> - create( - content = encryptedTags, - tags = - earlierVersion.tags - .filter { it.size > 1 && !(it[0] == key && it[1] == tag) } - .toTypedArray(), - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - } - } - } - - fun create( - content: String, - tags: Array>, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (PeopleListEvent) -> Unit, - ) { - val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { - tags - } else { - tags + AltTag.assemble(ALT) - } - - signer.sign(createdAt, KIND, newTags, content, onReady) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PinListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PinListEvent.kt index 0fdc7bd2c0..e92935e66f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PinListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PinListEvent.kt @@ -42,17 +42,16 @@ class PinListEvent( const val KIND = 33888 const val ALT = "Pinned Posts" - fun create( + suspend fun create( pins: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (PinListEvent) -> Unit, - ) { + ): PinListEvent { val tags = mutableListOf>() pins.forEach { tags.add(arrayOf("pin", it)) } tags.add(AltTag.assemble(ALT)) - signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) + return signer.sign(createdAt, KIND, tags.toTypedArray(), "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayBuilder.kt index 6152aa5652..453a7b44a6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayBuilder.kt @@ -21,194 +21,170 @@ package com.vitorpamplona.quartz.nip51Lists import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent class PrivateTagArrayBuilder { companion object { - fun create( + suspend fun create( tags: Array>, toPrivate: Boolean, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { + ): Pair>> = if (toPrivate) { - PrivateTagsInContent.encryptNip04( - privateTags = tags, - signer = signer, - ) { encryptedTags -> - onReady(encryptedTags, arrayOf()) - } + val encryptedTags = + PrivateTagsInContent.encryptNip04( + privateTags = tags, + signer = signer, + ) + Pair(encryptedTags, arrayOf()) } else { - onReady("", tags) + Pair("", tags) } - } - fun add( + suspend fun add( current: PrivateTagArrayEvent, newTag: Array, toPrivate: Boolean, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { + ): Pair>> = if (toPrivate) { - current.privateTags(signer) { privateTags -> + val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + val encryptedTags = PrivateTagsInContent.encryptNip04( privateTags = privateTags.plus(newTag), signer = signer, - ) { encryptedTags -> - onReady(encryptedTags, current.tags) - } - } + ) + Pair(encryptedTags, current.tags) } else { - onReady(current.content, current.tags.plus(newTag)) + Pair(current.content, current.tags.plus(newTag)) } - } - fun addAll( + suspend fun addAll( current: PrivateTagArrayEvent, newTag: Array>, toPrivate: Boolean, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { + ): Pair>> = if (toPrivate) { - current.privateTags(signer) { privateTags -> + val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + val encryptedTags = PrivateTagsInContent.encryptNip04( privateTags = privateTags.plus(newTag), signer = signer, - ) { encryptedTags -> - onReady(encryptedTags, current.tags) - } - } + ) + Pair(encryptedTags, current.tags) } else { - onReady(current.content, current.tags.plus(newTag)) + Pair(current.content, current.tags.plus(newTag)) } - } - fun replaceAllToPrivateNewTag( + suspend fun replaceAllToPrivateNewTag( dTag: String, current: PrivateTagArrayEvent?, oldTagStartsWith: Array, newTag: Array, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { + ): Pair>> = if (current == null) { - createPrivate(dTag, newTag, signer, onReady) + createPrivate(dTag, newTag, signer) } else { - replaceAllToPrivateNewTag(current, oldTagStartsWith, newTag, signer, onReady) + replaceAllToPrivateNewTag(current, oldTagStartsWith, newTag, signer) } - } - fun replaceAllToPublicNewTag( + suspend fun replaceAllToPublicNewTag( dTag: String, current: PrivateTagArrayEvent?, oldTagStartsWith: Array, newTag: Array, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { + ): Pair>> = if (current == null) { - createPublic(dTag, newTag, signer, onReady) + createPublic(dTag, newTag, signer) } else { - replaceAllToPublicNewTag(current, oldTagStartsWith, newTag, signer, onReady) + replaceAllToPublicNewTag(current, oldTagStartsWith, newTag, signer) } - } - fun replaceAllToPrivateNewTag( + suspend fun replaceAllToPrivateNewTag( current: PrivateTagArrayEvent, oldTagStartsWith: Array, newTag: Array, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { - current.privateTags(signer) { privateTags -> + ): Pair>> { + val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + val encryptedTags = PrivateTagsInContent.encryptNip04( privateTags = privateTags.replaceAll(oldTagStartsWith, newTag), signer = signer, - ) { encryptedTags -> - onReady(encryptedTags, current.tags.remove(oldTagStartsWith)) - } - } + ) + return Pair(encryptedTags, current.tags.remove(oldTagStartsWith)) } - fun replaceAllToPublicNewTag( + suspend fun replaceAllToPublicNewTag( current: PrivateTagArrayEvent, oldTagStartsWith: Array, newTag: Array, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { - current.privateTags(signer) { privateTags -> + ): Pair>> { + val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + val encryptedTags = PrivateTagsInContent.encryptNip04( privateTags = privateTags.remove(oldTagStartsWith), signer = signer, - ) { encryptedTags -> - onReady(encryptedTags, current.tags.remove(oldTagStartsWith).plus(newTag)) - } - } + ) + return Pair(encryptedTags, current.tags.remove(oldTagStartsWith).plus(newTag)) } - fun removeAllFromPrivate( + suspend fun removeAllFromPrivate( current: PrivateTagArrayEvent, oldTagStartsWith: Array, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { - current.privateTags(signer) { privateTags -> + ): Pair>> { + val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + val encryptedTags = PrivateTagsInContent.encryptNip04( privateTags = privateTags.remove(oldTagStartsWith), signer = signer, - ) { encryptedTags -> - onReady(encryptedTags, current.tags) - } - } + ) + return Pair(encryptedTags, current.tags) } fun removeAllFromPublic( current: PrivateTagArrayEvent, oldTagStartsWith: Array, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) = onReady(current.content, current.tags.remove(oldTagStartsWith)) + ): Pair>> = Pair(current.content, current.tags.remove(oldTagStartsWith)) - fun removeAll( + suspend fun removeAll( current: PrivateTagArrayEvent, oldTagStartsWith: Array, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { - current.privateTags(signer) { privateTags -> + ): Pair>> { + val privateTags = current.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + val encryptedTags = PrivateTagsInContent.encryptNip04( privateTags = privateTags.remove(oldTagStartsWith), signer = signer, - ) { encryptedTags -> - onReady(encryptedTags, current.tags.remove(oldTagStartsWith)) - } - } + ) + return Pair(encryptedTags, current.tags.remove(oldTagStartsWith)) } - fun createPrivate( + suspend fun createPrivate( dTag: String, newTag: Array, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { - PrivateTagsInContent.encryptNip04( - privateTags = arrayOf(newTag), - signer = signer, - ) { encryptedTags -> - onReady(encryptedTags, arrayOf(arrayOf("d", dTag))) - } + ): Pair>> { + val encryptedTags = + PrivateTagsInContent.encryptNip04( + privateTags = arrayOf(newTag), + signer = signer, + ) + return Pair(encryptedTags, arrayOf(arrayOf("d", dTag))) } fun createPublic( dTag: String, newTag: Array, signer: NostrSigner, - onReady: (content: String, tags: Array>) -> Unit, - ) { - onReady("", arrayOf(arrayOf("d", dTag), newTag)) - } + ): Pair>> = Pair("", arrayOf(arrayOf("d", dTag), newTag)) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt index fba112dd91..12d70ed5c8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEvent.kt @@ -20,14 +20,14 @@ */ package com.vitorpamplona.quartz.nip51Lists -import android.util.Log import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes +import kotlinx.coroutines.CancellationException @Immutable abstract class PrivateTagArrayEvent( @@ -35,61 +35,28 @@ abstract class PrivateTagArrayEvent( pubKey: HexKey, createdAt: Long, kind: Int, - tags: Array>, + tags: TagArray, content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient private var privateTagsCache: Array>? = null - - override fun countMemory(): Long = - super.countMemory() + - pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0) - override fun isContentEncoded() = true - fun cachedPrivateTags(): Array>? = privateTagsCache + suspend fun decrypt(signer: NostrSigner): TagArray { + if (signer.pubKey != pubKey) throw SignerExceptions.UnauthorizedDecryptionException() - fun privateTags( - signer: NostrSigner, - onReady: (Array>) -> Unit, - ) { - if (content.isEmpty()) { - onReady(emptyArray()) - return - } - - privateTagsCache?.let { - onReady(it) - return - } - - try { - PrivateTagsInContent.decrypt(content, signer) { - privateTagsCache = it - privateTagsCache?.let { onReady(it) } - } - } catch (e: Throwable) { - onReady(emptyArray()) - Log.w("GeneralList", "Error parsing the JSON ${e.message}") - } + return PrivateTagsInContent.decrypt(content, signer) } - fun mergeTagList( - signer: NostrSigner, - onReady: (Array>) -> Unit, - ) { - privateTags(signer) { - onReady(tags + it) + suspend fun privateTags(signer: NostrSigner): TagArray? { + if (signer.pubKey != pubKey) { + return null } - } - fun mapAllTags( - privateTags: Array>, - mapper: (Array) -> T, - ): Set { - val privateRooms = privateTags.mapNotNull(mapper) - val publicRooms = tags.mapNotNull(mapper) - - return (privateRooms + publicRooms).toSet() + return try { + PrivateTagsInContent.decrypt(content, signer) + } catch (e: Exception) { + if (e is CancellationException) throw e + null + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEventCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEventCache.kt new file mode 100644 index 0000000000..e2ac34b1a7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/PrivateTagArrayEventCache.kt @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists + +import androidx.collection.LruCache +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache +import kotlin.collections.plus + +class PrivateTagArrayEventCache( + signer: NostrSigner, + cacheSize: Int = 10, +) { + private val decryptionCache = + object : LruCache>(cacheSize) { + override fun create(key: T): PrivateTagArrayEventDecryptCache? = + if (key.content.isNotBlank() && key.pubKey == signer.pubKey) { + PrivateTagArrayEventDecryptCache(signer) + } else { + null + } + } + + fun remove(event: T) = decryptionCache.remove(event) + + fun cachedPrivateTags(event: T): TagArray? = decryptionCache[event]?.cached() + + suspend fun privateTags(event: T) = decryptionCache[event]?.decrypt(event) + + suspend fun mergeTagList(event: T): TagArray = event.tags + (privateTags(event) ?: emptyArray()) + + fun mergeTagListPrecached(event: T): TagArray = event.tags + (cachedPrivateTags(event) ?: emptyArray()) +} + +class PrivateTagArrayEventDecryptCache( + signer: NostrSigner, +) : DecryptCache(signer) { + override suspend fun decryptAndParse( + event: T, + signer: NostrSigner, + ): TagArray = event.decrypt(signer) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt deleted file mode 100644 index 4a21116809..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/RelaySetEvent.kt +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip51Lists - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip51Lists.tags.RelayTag -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class RelaySetEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun relays(): List = tags.mapNotNull(RelayTag::parse) - - fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1) - - companion object { - const val KIND = 30002 - const val ALT = "Relay list" - - fun create( - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (RelaySetEvent) -> Unit, - ) { - val tags = mutableListOf>() - relays.forEach { tags.add(arrayOf("r", it)) } - tags.add(AltTag.assemble(ALT)) - - signer.sign(createdAt, KIND, tags.toTypedArray(), "", onReady) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt index e364731150..e245632147 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip51Lists import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.utils.startsWith +import com.vitorpamplona.quartz.utils.startsWithAny inline fun TagArray.filterToArray(predicate: (Array) -> Boolean): TagArray = filterTo(ArrayList(), predicate).toTypedArray() @@ -29,6 +30,14 @@ inline fun TagArray.remove(predicate: (Array) -> Boolean): TagArray = fi fun TagArray.remove(startsWith: Array): TagArray = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray() +fun TagArray.removeAny(startsWith: List>): TagArray = + filterNotTo( + ArrayList(this.size), + { + it.startsWithAny(startsWith) + }, + ).toTypedArray() + fun TagArray.replaceAll( startsWith: Array, newElement: Array, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TrustedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TrustedRelayListEvent.kt deleted file mode 100644 index 22436fb974..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TrustedRelayListEvent.kt +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip51Lists - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip50Search.tags.RelayTag -import com.vitorpamplona.quartz.utils.TimeUtils - -@Immutable -class TrustedRelayListEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun relays(): List = tags.mapNotNull(RelayTag::parse) - - companion object { - const val KIND = 10089 - - fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) - - fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) - - fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) - - fun createTagArray(relays: List): Array> = - relays - .map { - RelayTag.assemble(it) - }.plusElement(AltTag.assemble("Relay list to use for trusted connections")) - .toTypedArray() - - fun updateRelayList( - earlierVersion: TrustedRelayListEvent, - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (TrustedRelayListEvent) -> Unit, - ) { - val tags = - earlierVersion.tags - .filter(RelayTag::notMatch) - .plus( - relays.map { - RelayTag.assemble(it) - }, - ).toTypedArray() - - signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady) - } - - fun createFromScratch( - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (TrustedRelayListEvent) -> Unit, - ) { - create(relays, signer, createdAt, onReady) - } - - fun create( - relays: List, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (TrustedRelayListEvent) -> Unit, - ) { - signer.sign(createdAt, KIND, createTagArray(relays), "", onReady) - } - - fun create( - relays: List, - signer: NostrSignerSync, - createdAt: Long = TimeUtils.now(), - ): TrustedRelayListEvent? = signer.sign(createdAt, KIND, createTagArray(relays), "") - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt new file mode 100644 index 0000000000..2aa6f41dc3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/BookmarkListEvent.kt @@ -0,0 +1,212 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.bookmarkList + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.EventBookmark +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.nip51Lists.tags.NameTag +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class BookmarkListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider { + override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint) + + override fun linkedEventIds() = tags.mapNotNull(EventBookmark::parseId) + + override fun addressHints() = tags.mapNotNull(AddressBookmark::parseAsHint) + + override fun linkedAddressIds() = tags.mapNotNull(AddressBookmark::parseAddressId) + + fun name() = tags.firstNotNullOfOrNull(NameTag::parse) + + fun countBookmarks() = tags.count(BookmarkIdTag::isTagged) + + fun publicBookmarks(): List = tags.mapNotNull(BookmarkIdTag::parse) + + suspend fun privateBookmarks(signer: NostrSigner): List? = privateTags(signer)?.mapNotNull(BookmarkIdTag::parse) + + companion object { + const val KIND = 30001 + const val ALT = "List of bookmarks" + const val DEFAULT_D_TAG_BOOKMARKS = "bookmark" + + fun createBookmarkAddress(pubKey: HexKey) = Address(KIND, pubKey, DEFAULT_D_TAG_BOOKMARKS) + + suspend fun create( + bookmarkIdTag: BookmarkIdTag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): BookmarkListEvent = + if (isPrivate) { + create( + publicBookmarks = emptyList(), + privateBookmarks = listOf(bookmarkIdTag), + signer = signer, + createdAt = createdAt, + ) + } else { + create( + publicBookmarks = listOf(bookmarkIdTag), + privateBookmarks = emptyList(), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun add( + earlierVersion: BookmarkListEvent, + bookmarkIdTag: BookmarkIdTag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): BookmarkListEvent = + if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + resign( + tags = earlierVersion.tags, + privateTags = privateTags.plus(bookmarkIdTag.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } else { + resign( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(bookmarkIdTag.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun remove( + earlierVersion: BookmarkListEvent, + bookmarkIdTag: BookmarkIdTag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): BookmarkListEvent = + if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + resign( + privateTags = privateTags.remove(bookmarkIdTag.toTagIdOnly()), + tags = earlierVersion.tags.remove(bookmarkIdTag.toTagIdOnly()), + signer = signer, + createdAt = createdAt, + ) + } else { + resign( + content = earlierVersion.content, + tags = + earlierVersion.tags.remove(bookmarkIdTag.toTagIdOnly()), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun resign( + tags: TagArray, + privateTags: TagArray, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = resign( + content = PrivateTagsInContent.encryptNip04(privateTags, signer), + tags = tags, + signer = signer, + createdAt = createdAt, + ) + + suspend fun resign( + content: String, + tags: TagArray, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): BookmarkListEvent { + val newTags = + if (tags.fastAny(AltTag::match)) { + tags + } else { + tags + AltTag.assemble(ALT) + } + + return signer.sign(createdAt, KIND, newTags, content) + } + + suspend fun create( + name: String = "", + publicBookmarks: List = emptyList(), + privateBookmarks: List = emptyList(), + dTag: String = DEFAULT_D_TAG_BOOKMARKS, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): BookmarkListEvent { + val template = build(name, publicBookmarks, privateBookmarks, signer, dTag, createdAt) + return signer.sign(template) + } + + suspend fun build( + name: String = "", + publicBookmarks: List = emptyList(), + privateBookmarks: List = emptyList(), + signer: NostrSigner, + dTag: String = DEFAULT_D_TAG_BOOKMARKS, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip04(privateBookmarks.map { it.toTagArray() }.toTypedArray(), signer), + createdAt = createdAt, + ) { + dTag(dTag) + alt(ALT) + name(name) + bookmarks(publicBookmarks) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..3fc8ebe461 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/TagArrayBuilderExt.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.bookmarkList + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag +import com.vitorpamplona.quartz.nip51Lists.tags.NameTag + +fun TagArrayBuilder.name(name: String) = addUnique(NameTag.assemble(name)) + +fun TagArrayBuilder.bookmarks(bookmarks: List) = addAll(bookmarks.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt new file mode 100644 index 0000000000..ede0ece456 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.ensure +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +class AddressBookmark( + val address: Address, + val relayHint: NormalizedRelayUrl? = null, +) : BookmarkIdTag { + fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) + + fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) + + override fun toTagArray() = assemble(address, relayHint) + + override fun toTagIdOnly() = assemble(address, null) + + companion object { + const val TAG_NAME = "a" + + @JvmStatic + fun isTagged(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun isTagged( + tag: Array, + addressId: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == addressId + + @JvmStatic + fun isTagged( + tag: Array, + address: AddressBookmark, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag() + + @JvmStatic + fun isIn( + tag: Array, + addressIds: Set, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds + + @JvmStatic + fun isTaggedWithKind( + tag: Array, + kind: String, + ) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], kind) + + @JvmStatic + fun parse( + aTagId: String, + relay: String?, + ) = Address.parse(aTagId)?.let { + AddressBookmark(it, relay?.let { RelayUrlNormalizer.normalizeOrNull(it) }) + } + + @JvmStatic + fun parse(tag: Array): AddressBookmark? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return parse(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun parseValidAddress(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return Address.parse(tag[1])?.toValue() + } + + @JvmStatic + fun parseAddress(tag: Array): Address? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return Address.parse(tag[1]) + } + + @JvmStatic + fun parseAddressId(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + @JvmStatic + fun parseAsHint(tag: Array): AddressHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + ensure(tag[1].contains(':')) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return AddressHint(tag[1], relayHint) + } + + @JvmStatic + fun assemble( + aTagId: HexKey, + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url) + + @JvmStatic + fun assemble( + address: Address, + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, address.toValue(), relay?.url) + + @JvmStatic + fun assemble( + kind: Int, + pubKey: String, + dTag: String, + relay: NormalizedRelayUrl?, + ) = assemble(Address.assemble(kind, pubKey, dTag), relay) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/BookmarkIdTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/BookmarkIdTag.kt new file mode 100644 index 0000000000..a3f344e459 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/BookmarkIdTag.kt @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags + +import com.vitorpamplona.quartz.nip01Core.core.Tag + +sealed interface BookmarkIdTag { + fun toTagArray(): Tag + + fun toTagIdOnly(): Tag + + companion object { + fun isTagged(tag: Array) = EventBookmark.isTagged(tag) || AddressBookmark.isTagged(tag) + + fun parse(tag: Array): BookmarkIdTag? = EventBookmark.parse(tag) ?: AddressBookmark.parse(tag) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt new file mode 100644 index 0000000000..8bfeb5a591 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.ensure +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +@Immutable +class EventBookmark( + val eventId: HexKey, + val relay: NormalizedRelayUrl? = null, + val author: HexKey? = null, +) : BookmarkIdTag { + fun countMemory(): Long = + 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) + eventId.bytesUsedInMemory() + + (relay?.url?.bytesUsedInMemory() ?: 0) + + (author?.bytesUsedInMemory() ?: 0) + + fun toNEvent(): String = NEvent.create(eventId, author, null, relay) + + override fun toTagArray() = assemble(eventId, relay, author) + + override fun toTagIdOnly() = assemble(eventId, null, null) + + companion object { + const val TAG_NAME = "e" + + @JvmStatic + fun isTagged(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64 + + @JvmStatic + fun isTagged( + tag: Array, + eventId: HexKey, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == eventId + + @JvmStatic + fun parse(tag: Array): EventBookmark? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + return EventBookmark(tag[1], pickRelayHint(tag), pickAuthor(tag)) + } + + @JvmStatic + fun parseId(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + return tag[1] + } + + @JvmStatic + private fun pickRelayHint(tag: Array): NormalizedRelayUrl? { + if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2]) + if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3]) + if (tag.has(4) && tag[4].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[4])) return RelayUrlNormalizer.normalizeOrNull(tag[4]) + return null + } + + @JvmStatic + private fun pickAuthor(tag: Array): HexKey? { + if (tag.has(2) && tag[2].length == 64) return tag[2] + if (tag.has(3) && tag[3].length == 64) return tag[3] + if (tag.has(4) && tag[4].length == 64) return tag[4] + return null + } + + @JvmStatic + fun parseAsHint(tag: Array): EventIdHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val hint = pickRelayHint(tag) + + ensure(hint != null) { return null } + + return EventIdHint(tag[1], hint) + } + + @JvmStatic + fun assemble( + eventId: HexKey, + relay: NormalizedRelayUrl?, + author: HexKey?, + ) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, author) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/NostrSignerExt.kt similarity index 59% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/NostrSignerExt.kt index dc90d7b7df..7525a1a1d9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/SignResultProcessor.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/NostrSignerExt.kt @@ -18,24 +18,32 @@ * 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.nip55AndroidSigner.api.foreground.processors +package com.vitorpamplona.quartz.nip51Lists.encryption -import android.content.Intent import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.SignResponse +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -class SignResultProcessor( - val unsignedEvent: Event, - val onReady: (Event) -> Unit, -) : NewResultProcessor { - override fun consume(intent: Intent) { - val foregroundResult = SignResponse.parse(intent, unsignedEvent) +suspend fun NostrSigner.signNip51List( + createdAt: Long, + kind: Int, + tags: Array>, + privateTags: Array>, +) = sign( + createdAt = createdAt, + kind = kind, + tags = tags, + content = PrivateTagsInContent.encryptNip44(privateTags, this), +) - if (foregroundResult is SignerResult.Successful) { - onReady(foregroundResult.result.event) - } - } -} +fun NostrSignerSync.signNip51List( + createdAt: Long, + kind: Int, + tags: Array>, + privateTags: Array>, +) = sign( + createdAt = createdAt, + kind = kind, + tags = tags, + content = PrivateTagsInContent.encryptNip44(privateTags, this), +) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.kt index 123dbb7f7f..fec602548c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/encryption/PrivateTagsInContent.kt @@ -20,44 +20,76 @@ */ package com.vitorpamplona.quartz.nip51Lists.encryption +import android.util.Log +import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync class PrivateTagsInContent { companion object { fun decode(content: String) = JsonMapper.mapper.readValue>>(content) - fun encode(privateTags: Array>) = JsonMapper.mapper.writeValueAsString(privateTags) + fun encode(privateTags: Array>): String = JsonMapper.mapper.writeValueAsString(privateTags) - fun decrypt( + suspend fun decrypt( content: String, signer: NostrSigner, - onReady: (Array>) -> Unit, - ) { - signer.decrypt(content, signer.pubKey) { - onReady(decode(it)) + ): TagArray { + if (content.isBlank()) return emptyArray() + val json = signer.decrypt(content, signer.pubKey) + return try { + decode(json) + } catch (e: JsonParseException) { + Log.w("DraftEvent", "Unable to parse inner event of a draft: $json") + throw e } } + suspend fun encryptNip04( + privateTags: Array>? = null, + signer: NostrSigner, + ): String = + signer.nip04Encrypt( + if (privateTags.isNullOrEmpty()) "" else encode(privateTags), + signer.pubKey, + ) + + suspend fun encryptNip44( + privateTags: Array>? = null, + signer: NostrSigner, + ): String = + signer.nip44Encrypt( + if (privateTags.isNullOrEmpty()) "" else encode(privateTags), + signer.pubKey, + ) + + suspend fun decrypt( + content: String, + signer: NostrSignerSync, + ): TagArray { + val json = signer.decrypt(content, signer.pubKey) + return decode(json) + } + fun encryptNip04( privateTags: Array>? = null, - signer: NostrSigner, - onReady: (String) -> Unit, - ) = signer.nip04Encrypt( - if (privateTags.isNullOrEmpty()) "" else encode(privateTags), - signer.pubKey, - onReady, - ) + signer: NostrSignerSync, + ): String = + signer.nip04Encrypt( + if (privateTags.isNullOrEmpty()) "" else encode(privateTags), + signer.pubKey, + ) fun encryptNip44( privateTags: Array>? = null, - signer: NostrSigner, - onReady: (String) -> Unit, - ) = signer.nip44Encrypt( - if (privateTags.isNullOrEmpty()) "" else encode(privateTags), - signer.pubKey, - onReady, - ) + signer: NostrSignerSync, + ): String = + signer.nip44Encrypt( + if (privateTags.isNullOrEmpty()) "" else encode(privateTags), + signer.pubKey, + ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/FollowListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/FollowListEvent.kt similarity index 55% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/FollowListEvent.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/FollowListEvent.kt index 00fb55b4ed..fcecb05dbc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/FollowListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/FollowListEvent.kt @@ -18,19 +18,27 @@ * 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.nip51Lists +package com.vitorpamplona.quartz.nip51Lists.followList import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.remove import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag -import com.vitorpamplona.quartz.nip51Lists.tags.NameTag import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag import com.vitorpamplona.quartz.utils.TimeUtils +import java.util.UUID +import kotlin.collections.plus @Immutable class FollowListEvent( @@ -40,102 +48,120 @@ class FollowListEvent( tags: Array>, content: String, sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - fun pubKeys() = tags.mapNotNull(PTag::parseKey) +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint) - fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) - - fun name() = tags.firstNotNullOfOrNull(NameTag::parse) + override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey) fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse) - fun nameOrTitle() = name() ?: title() - fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) + fun follows() = tags.follows() + + fun followIds() = tags.followIds() + + fun followIdSet() = tags.followIdSet() + companion object { const val KIND = 39089 const val ALT = "List of people to follow" - fun createListWithUser( + suspend fun create( name: String, - pubKeyHex: String, + person: UserTag, signer: NostrSigner, + dTag: String = UUID.randomUUID().toString(), createdAt: Long = TimeUtils.now(), - onReady: (FollowListEvent) -> Unit, - ) { + ): FollowListEvent = create( - content = "", - tags = arrayOf(arrayOf("d", name), arrayOf("p", pubKeyHex)), + name = name, + people = listOf(person), signer = signer, + dTag = dTag, createdAt = createdAt, - onReady = onReady, ) - } - fun addUsers( + suspend fun addUsers( earlierVersion: FollowListEvent, - listPubKeyHex: List, + people: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (FollowListEvent) -> Unit, - ) { - create( + ): FollowListEvent = + resign( content = earlierVersion.content, - tags = - earlierVersion.tags.plus( - listPubKeyHex.map { arrayOf("p", it) }, - ), + tags = earlierVersion.tags.plus(people.map { it.toTagArray() }), signer = signer, createdAt = createdAt, - onReady = onReady, ) - } - fun addUser( + suspend fun add( earlierVersion: FollowListEvent, - pubKeyHex: String, + person: UserTag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (FollowListEvent) -> Unit, - ) = addUsers(earlierVersion, listOf(pubKeyHex), signer, createdAt, onReady) + ) = addUsers(earlierVersion, listOf(person), signer, createdAt) - fun removeUser( + suspend fun remove( earlierVersion: FollowListEvent, - pubKeyHex: String, + person: UserTag, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (FollowListEvent) -> Unit, - ) { - create( + ): FollowListEvent = + resign( content = earlierVersion.content, - tags = - earlierVersion.tags - .filter { it.size > 1 && !(it[0] == "p" && it[1] == pubKeyHex) } - .toTypedArray(), + tags = earlierVersion.tags.remove(person.toTagIdOnly()), signer = signer, createdAt = createdAt, - onReady = onReady, ) - } - fun create( + suspend fun resign( content: String, tags: Array>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (FollowListEvent) -> Unit, - ) { + ): FollowListEvent { val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { + if (tags.fastAny(AltTag::match)) { tags } else { tags + AltTag.assemble(ALT) } - signer.sign(createdAt, KIND, newTags, content, onReady) + return signer.sign(createdAt, KIND, newTags, content) + } + + suspend fun create( + name: String, + people: List = emptyList(), + signer: NostrSigner, + dTag: String = UUID.randomUUID().toString(), + createdAt: Long = TimeUtils.now(), + ): FollowListEvent { + val template = build(name, people, dTag, createdAt) + return signer.sign(template) + } + + fun build( + name: String, + people: List = emptyList(), + dTag: String = UUID.randomUUID().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = "", + createdAt = createdAt, + ) { + dTag(dTag) + alt(ALT) + name(name) + people(people) + + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..511395f544 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayBuilderExt.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.followList + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.tags.NameTag + +fun TagArrayBuilder.name(name: String) = addUnique(NameTag.assemble(name)) + +fun TagArrayBuilder.people(peoples: List) = addAll(peoples.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayExt.kt new file mode 100644 index 0000000000..191b6e869f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/followList/TagArrayExt.kt @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.followList + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag + +fun TagArray.follows() = mapNotNull(UserTag::parse) + +fun TagArray.followIds() = mapNotNull(UserTag::parseKey) + +fun TagArray.followIdSet() = mapNotNullTo(mutableSetOf(), UserTag::parseKey) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/GeohashListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/GeohashListEvent.kt new file mode 100644 index 0000000000..622e2767e3 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/GeohashListEvent.kt @@ -0,0 +1,220 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.geohashList + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.nip51Lists.removeAny +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class GeohashListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicGeohashes() = tags.geohashList() + + suspend fun decryptPrivateGeohashes(signer: NostrSigner) = privateTags(signer)?.geohashList() + + suspend fun decryptGeohashes(signer: NostrSigner): List = publicGeohashes() + (decryptPrivateGeohashes(signer) ?: emptyList()) + + companion object { + const val KIND = 10081 + const val ALT = "Geohash List" + const val FIXED_D_TAG = "" + + fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) + + suspend fun create( + geohash: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = create( + geohashes = listOf(geohash), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + ) + + suspend fun create( + geohashes: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): GeohashListEvent = + if (isPrivate) { + create( + publicGeohashes = emptyList(), + privateGeohashes = geohashes, + signer = signer, + createdAt = createdAt, + ) + } else { + create( + publicGeohashes = geohashes, + privateGeohashes = emptyList(), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun add( + earlierVersion: GeohashListEvent, + geohash: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = add( + earlierVersion = earlierVersion, + geohashes = listOf(geohash), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + ) + + suspend fun add( + earlierVersion: GeohashListEvent, + geohashes: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): GeohashListEvent { + val geohashTags = geohashes.map { GeoHashTag.assembleSingle(it) } + return if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + resign( + tags = earlierVersion.tags, + privateTags = privateTags.removeAny(geohashTags) + geohashTags, + signer = signer, + createdAt = createdAt, + ) + } else { + resign( + content = earlierVersion.content, + tags = earlierVersion.tags.removeAny(geohashTags) + geohashTags, + signer = signer, + createdAt = createdAt, + ) + } + } + + suspend fun remove( + earlierVersion: GeohashListEvent, + geohash: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): GeohashListEvent { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + return resign( + privateTags = privateTags.remove(GeoHashTag.assembleSingle(geohash)), + tags = earlierVersion.tags.remove(GeoHashTag.assembleSingle(geohash)), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun resign( + tags: TagArray, + privateTags: TagArray, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = resign( + content = PrivateTagsInContent.encryptNip04(privateTags, signer), + tags = tags, + signer = signer, + createdAt = createdAt, + ) + + suspend fun resign( + content: String, + tags: TagArray, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): GeohashListEvent { + val newTags = + if (tags.fastAny(AltTag::match)) { + tags + } else { + tags + AltTag.assemble(ALT) + } + + return signer.sign(createdAt, KIND, newTags, content) + } + + suspend fun create( + publicGeohashes: List = emptyList(), + privateGeohashes: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): GeohashListEvent { + val template = build(publicGeohashes, privateGeohashes, signer, createdAt) + return signer.sign(template) + } + + fun create( + publicGeohashes: List = emptyList(), + privateGeohashes: List = emptyList(), + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): GeohashListEvent { + val privateTagArray = publicGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray() + val publicTagArray = privateGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray() + AltTag.assemble(ALT) + return signer.signNip51List(createdAt, KIND, publicTagArray, privateTagArray) + } + + suspend fun build( + publicGeohashes: List = emptyList(), + privateGeohashes: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip04(privateGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + geohashes(publicGeohashes) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/TagArrayBuilderExt.kt similarity index 88% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/TagArrayBuilderExt.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/TagArrayBuilderExt.kt index e91af0ce2f..b10d923486 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/TagArrayBuilderExt.kt @@ -18,9 +18,11 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.locations +package com.vitorpamplona.quartz.nip51Lists.geohashList import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag fun TagArrayBuilder.followGeohash(geohash: String) = add(GeoHashTag.assembleSingle(geohash)) + +fun TagArrayBuilder.geohashes(geohash: List) = addAll(geohash.map { GeoHashTag.assembleSingle(it) }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/TagArrayExt.kt new file mode 100644 index 0000000000..efaeb4292e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/geohashList/TagArrayExt.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.geohashList + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag + +fun TagArray.geohashList() = mapNotNull(GeoHashTag::parse) + +fun TagArray.geohashSet() = mapNotNullTo(mutableSetOf(), GeoHashTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/HashtagListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/HashtagListEvent.kt new file mode 100644 index 0000000000..1fc7071206 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/HashtagListEvent.kt @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.hashtagList + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.nip51Lists.removeAny +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.map + +@Immutable +class HashtagListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicHashtags() = tags.mapNotNull(HashtagTag::parse) + + companion object { + const val KIND = 10015 + const val ALT = "Hashtag List" + const val FIXED_D_TAG = "" + + fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) + + suspend fun create( + hashtag: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = create( + hashtags = listOf(hashtag), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + ) + + suspend fun create( + hashtags: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): HashtagListEvent = + if (isPrivate) { + create( + publicHashtags = emptyList(), + privateHashtags = hashtags, + signer = signer, + createdAt = createdAt, + ) + } else { + create( + publicHashtags = hashtags, + privateHashtags = emptyList(), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun add( + earlierVersion: HashtagListEvent, + hashtag: String, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = add( + earlierVersion = earlierVersion, + hashtags = listOf(hashtag), + isPrivate = isPrivate, + signer = signer, + createdAt = createdAt, + ) + + suspend fun add( + earlierVersion: HashtagListEvent, + hashtags: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): HashtagListEvent { + val hashtags = hashtags.map { HashtagTag.assemble(it) } + return if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + resign( + tags = earlierVersion.tags, + privateTags = privateTags.removeAny(hashtags) + hashtags, + signer = signer, + createdAt = createdAt, + ) + } else { + resign( + content = earlierVersion.content, + tags = earlierVersion.tags.removeAny(hashtags) + hashtags, + signer = signer, + createdAt = createdAt, + ) + } + } + + suspend fun remove( + earlierVersion: HashtagListEvent, + hashtag: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): HashtagListEvent { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + return resign( + privateTags = privateTags.remove(HashtagTag.assemble(hashtag)), + tags = earlierVersion.tags.remove(HashtagTag.assemble(hashtag)), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun resign( + tags: TagArray, + privateTags: TagArray, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = resign( + content = PrivateTagsInContent.encryptNip04(privateTags, signer), + tags = tags, + signer = signer, + createdAt = createdAt, + ) + + suspend fun resign( + content: String, + tags: TagArray, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): HashtagListEvent { + val newTags = + if (tags.fastAny(AltTag::match)) { + tags + } else { + tags + AltTag.assemble(ALT) + } + + return signer.sign(createdAt, KIND, newTags, content) + } + + suspend fun create( + publicHashtags: List = emptyList(), + privateHashtags: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): HashtagListEvent { + val template = build(publicHashtags, privateHashtags, signer, createdAt) + return signer.sign(template) + } + + fun create( + publicHashtags: List = emptyList(), + privateHashtags: List = emptyList(), + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): HashtagListEvent { + val privateTagArray = publicHashtags.map { HashtagTag.assemble(it) }.toTypedArray() + val publicTagArray = privateHashtags.map { HashtagTag.assemble(it) }.toTypedArray() + AltTag.assemble(ALT) + return signer.signNip51List(createdAt, KIND, publicTagArray, privateTagArray) + } + + suspend fun build( + publicHashtags: List = emptyList(), + privateHashtags: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip04(privateHashtags.map { HashtagTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + hashtags(publicHashtags) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayBuilderExt.kt similarity index 84% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/TagArrayBuilderExt.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayBuilderExt.kt index 4fca44c0e3..e6168ce2fc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayBuilderExt.kt @@ -18,9 +18,12 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip51Lists.interests +package com.vitorpamplona.quartz.nip51Lists.hashtagList import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtag fun TagArrayBuilder.followHashTag(hashtag: String) = add(HashtagTag.assemble(hashtag)) + +fun TagArrayBuilder.hashtags(hashtags: List) = addAll(hashtags.map { HashtagTag.assemble(it) }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayExt.kt new file mode 100644 index 0000000000..7236e78713 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/hashtagList/TagArrayExt.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.hashtagList + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag + +fun TagArray.hashtagList() = mapNotNull(HashtagTag::parse) + +fun TagArray.hashtagSet() = mapNotNullTo(mutableSetOf(), HashtagTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt deleted file mode 100644 index 3e85f0797b..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/interests/HashtagListEvent.kt +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip51Lists.interests - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag -import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder -import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent -import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.tryAndWait -import kotlin.coroutines.resume - -@Immutable -class HashtagListEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient var publicAndPrivateHashtagCache: Set? = null - - fun publicHashtags() = tags.mapNotNull(HashtagTag::parse) - - fun publicAndCachedPrivateHashtags() = publicHashtags().toSet() + (publicAndPrivateHashtagCache ?: emptySet()) - - fun publicAndPrivateHashtag( - signer: NostrSigner, - onReady: (Set) -> Unit, - ) { - publicAndPrivateHashtagCache?.let { eventList -> - onReady(eventList) - return - } - - mergeTagList(signer) { - val set = it.mapNotNull(HashtagTag::parse).toSet() - publicAndPrivateHashtagCache = set - onReady(set) - } - } - - suspend fun publicAndPrivateHashtag(signer: NostrSigner): Set? { - publicAndPrivateHashtagCache?.let { return it } - - return tryAndWait { continuation -> - publicAndPrivateHashtag(signer) { privateTagList -> - continuation.resume(privateTagList) - } - } - } - - companion object { - const val KIND = 10015 - const val ALT = "Hashtag List" - const val FIXED_D_TAG = "" - - fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) - - private fun createHashtagBase( - tags: Array>, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (HashtagListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.create( - tags, - isPrivate, - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - fun createHashtag( - hashtag: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (HashtagListEvent) -> Unit, - ) = createHashtagBase( - tags = arrayOf(HashtagTag.assemble(hashtag)), - isPrivate = isPrivate, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - - fun createHashtags( - hashtags: List, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (HashtagListEvent) -> Unit, - ) = createHashtagBase( - tags = HashtagTag.assemble(hashtags).toTypedArray(), - isPrivate = isPrivate, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - - fun removeHashtag( - earlierVersion: HashtagListEvent, - hashtag: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (HashtagListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.removeAll( - earlierVersion, - HashtagTag.assemble(hashtag), - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - private fun addHashtagBase( - earlierVersion: HashtagListEvent, - newTags: Array>, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (HashtagListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.addAll( - earlierVersion, - newTags, - isPrivate, - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - fun addHashtag( - earlierVersion: HashtagListEvent, - hashtag: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (HashtagListEvent) -> Unit, - ) = addHashtagBase( - earlierVersion, - arrayOf(HashtagTag.assemble(hashtag)), - isPrivate, - signer, - createdAt, - onReady, - ) - - fun addHashtags( - earlierVersion: HashtagListEvent, - hashtags: List, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (HashtagListEvent) -> Unit, - ) = addHashtagBase( - earlierVersion, - HashtagTag.assemble(hashtags).toTypedArray(), - isPrivate, - signer, - createdAt, - onReady, - ) - - private fun create( - content: String, - tags: Array>, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (HashtagListEvent) -> Unit, - ) { - val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { - tags - } else { - tags + AltTag.assemble(ALT) - } - - signer.sign(createdAt, KIND, newTags, content, onReady) - } - - fun create( - hashtags: List, - signer: NostrSignerSync, - createdAt: Long = TimeUtils.now(), - ): HashtagListEvent? { - val tags = HashtagTag.assemble(hashtags).toTypedArray() - return signer.sign(createdAt, KIND, tags, "") - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt deleted file mode 100644 index d21b0239f8..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/locations/GeohashListEvent.kt +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip51Lists.locations - -import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHashTag -import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder -import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent -import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.tryAndWait -import kotlin.coroutines.resume - -@Immutable -class GeohashListEvent( - id: HexKey, - pubKey: HexKey, - createdAt: Long, - tags: Array>, - content: String, - sig: HexKey, -) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient var publicAndPrivateGeohashCache: Set? = null - - fun publicGeohashes() = tags.mapNotNull(GeoHashTag::parse) - - fun publicAndCachedPrivateGeohash() = publicGeohashes().toSet() + (publicAndPrivateGeohashCache ?: emptySet()) - - fun publicAndPrivateGeohash( - signer: NostrSigner, - onReady: (Set) -> Unit, - ) { - publicAndPrivateGeohashCache?.let { eventList -> - onReady(eventList) - return - } - - mergeTagList(signer) { - val set = it.mapNotNull(GeoHashTag::parse).toSet() - publicAndPrivateGeohashCache = set - onReady(set) - } - } - - suspend fun publicAndPrivateGeohash(signer: NostrSigner): Set? { - publicAndPrivateGeohashCache?.let { return it } - - return tryAndWait { continuation -> - publicAndPrivateGeohash(signer) { privateTagList -> - continuation.resume(privateTagList) - } - } - } - - companion object { - const val KIND = 10081 - const val ALT = "Geohash List" - const val FIXED_D_TAG = "" - - fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) - - private fun createGeohashBase( - tags: Array>, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (GeohashListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.create( - tags, - isPrivate, - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - fun createGeohash( - geohash: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (GeohashListEvent) -> Unit, - ) = createGeohashBase( - tags = arrayOf(GeoHashTag.assembleSingle(geohash)), - isPrivate = isPrivate, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - - fun createGeohashs( - geohashs: List, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (GeohashListEvent) -> Unit, - ) = createGeohashBase( - tags = geohashs.map { GeoHashTag.assembleSingle(it) }.toTypedArray(), - isPrivate = isPrivate, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - - fun removeGeohash( - earlierVersion: GeohashListEvent, - geohash: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (GeohashListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.removeAll( - earlierVersion, - GeoHashTag.assembleSingle(geohash), - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - private fun addGeohashBase( - earlierVersion: GeohashListEvent, - newTags: Array>, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (GeohashListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.addAll( - earlierVersion, - newTags, - isPrivate, - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - fun addGeohash( - earlierVersion: GeohashListEvent, - geohash: String, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (GeohashListEvent) -> Unit, - ) = addGeohashBase( - earlierVersion, - arrayOf(GeoHashTag.assembleSingle(geohash)), - isPrivate, - signer, - createdAt, - onReady, - ) - - fun addGeohashs( - earlierVersion: GeohashListEvent, - geohashs: List, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (GeohashListEvent) -> Unit, - ) = addGeohashBase( - earlierVersion, - geohashs.map { GeoHashTag.assembleSingle(it) }.toTypedArray(), - isPrivate, - signer, - createdAt, - onReady, - ) - - private fun create( - content: String, - tags: Array>, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (GeohashListEvent) -> Unit, - ) { - val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { - tags - } else { - tags + AltTag.assemble(ALT) - } - - signer.sign(createdAt, KIND, newTags, content, onReady) - } - - fun create( - geohashs: List, - signer: NostrSignerSync, - createdAt: Long = TimeUtils.now(), - ): GeohashListEvent? { - val tags = geohashs.map { GeoHashTag.assembleSingle(it) }.toTypedArray() - return signer.sign(createdAt, KIND, tags, "") - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/MuteListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/MuteListEvent.kt new file mode 100644 index 0000000000..70055d4d93 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/MuteListEvent.kt @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.muteList + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.plus + +@Immutable +class MuteListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig), + PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey) + + fun countMutes() = tags.count(MuteTag::isTagged) + + fun publicMutes(): List = tags.mapNotNull(MuteTag::parse) + + suspend fun privateMutes(signer: NostrSigner): List? = privateTags(signer)?.mapNotNull(MuteTag::parse) + + override fun dTag() = FIXED_D_TAG + + companion object { + const val KIND = 10000 + const val FIXED_D_TAG = "" + const val ALT = "Mute List" + + fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) + + fun blockListFor(pubKeyHex: HexKey): String = "10000:$pubKeyHex:" + + suspend fun create( + mute: MuteTag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): MuteListEvent = + if (isPrivate) { + create( + publicMutes = emptyList(), + privateMutes = listOf(mute), + signer = signer, + createdAt = createdAt, + ) + } else { + create( + publicMutes = listOf(mute), + privateMutes = emptyList(), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun add( + earlierVersion: MuteListEvent, + mute: MuteTag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): MuteListEvent = + if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + resign( + publicTags = earlierVersion.tags, + privateTags = privateTags.plus(mute.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } else { + resign( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(mute.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun remove( + earlierVersion: MuteListEvent, + mute: MuteTag, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): MuteListEvent { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + + return resign( + privateTags = privateTags.remove(mute.toTagIdOnly()), + publicTags = earlierVersion.tags.remove(mute.toTagIdOnly()), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun resign( + publicTags: TagArray, + privateTags: TagArray, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = resign( + content = PrivateTagsInContent.encryptNip04(privateTags, signer), + tags = publicTags, + signer = signer, + createdAt = createdAt, + ) + + suspend fun resign( + content: String, + tags: Array>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): MuteListEvent { + val newTags = + if (tags.fastAny(AltTag::match)) { + tags + } else { + tags + AltTag.assemble(ALT) + } + + return signer.sign(createdAt, KIND, newTags, content) + } + + suspend fun create( + publicMutes: List = emptyList(), + privateMutes: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): MuteListEvent { + val template = build(publicMutes, privateMutes, signer, createdAt) + return signer.sign(template) + } + + suspend fun build( + publicMutes: List = emptyList(), + privateMutes: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip04(privateMutes.map { it.toTagArray() }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + mutes(publicMutes) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..4d0fdaa2fb --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.muteList + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag + +fun TagArrayBuilder.mutes(mutes: List) = addAll(mutes.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/TagArrayExt.kt new file mode 100644 index 0000000000..1b918a68ef --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/TagArrayExt.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.muteList + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag + +fun TagArray.mutedUsersAndWords() = mapNotNull(MuteTag::parse) + +fun TagArray.mutedUsers() = mapNotNull(UserTag::parse) + +fun TagArray.mutedUserIds() = mapNotNull(UserTag::parseKey) + +fun TagArray.mutedUserIdSet() = mapNotNullTo(mutableSetOf(), UserTag::parseKey) + +fun TagArray.mutedWords() = mapNotNull(WordTag::parse) + +fun TagArray.mutedWordSet() = mapNotNullTo(mutableSetOf(), WordTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/MuteTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/MuteTag.kt new file mode 100644 index 0000000000..a6a65786c9 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/MuteTag.kt @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.muteList.tags + +import com.vitorpamplona.quartz.nip01Core.core.Tag + +sealed interface MuteTag { + fun toTagArray(): Tag + + fun toTagIdOnly(): Tag + + companion object { + fun isTagged(tag: Array) = WordTag.isTagged(tag) || UserTag.isTagged(tag) + + fun parse(tag: Array): MuteTag? = WordTag.parse(tag) ?: UserTag.parse(tag) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt new file mode 100644 index 0000000000..2558e6d542 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.muteList.tags + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.toNpub +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.ensure +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +class UserTag( + val pubKey: HexKey, + val relayHint: NormalizedRelayUrl? = null, +) : MuteTag { + fun countMemory(): Long = + 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) + pubKey.bytesUsedInMemory() + + (relayHint?.url?.bytesUsedInMemory() ?: 0) + + fun toNProfile(): String = NProfile.create(pubKey, relayHint?.let { listOf(it) } ?: emptyList()) + + fun toNPub(): String = pubKey.hexToByteArray().toNpub() + + override fun toTagArray() = assemble(pubKey, relayHint) + + override fun toTagIdOnly() = assemble(pubKey, null) + + companion object { + const val TAG_NAME = "p" + + fun isTagged(tag: Array): Boolean = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64 + + fun isTagged( + tag: Array, + key: HexKey, + ): Boolean = tag.has(1) && tag[0] == TAG_NAME && tag[1] == key + + @JvmStatic + fun parse(tag: Tag): UserTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return UserTag(tag[1], hint) + } + + @JvmStatic + fun parseKey(tag: Array): HexKey? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + return tag[1] + } + + @JvmStatic + fun parseAsHint(tag: Array): PubKeyHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + + ensure(hint != null) { return null } + + return PubKeyHint(tag[1], hint) + } + + @JvmStatic + fun assemble( + pubkey: HexKey, + relayHint: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint?.url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt similarity index 54% rename from quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/tags/RelayTag.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt index db8babc105..1204b5d422 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/tags/RelayTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt @@ -18,35 +18,57 @@ * 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.nip50Search.tags +package com.vitorpamplona.quartz.nip51Lists.muteList.tags +import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.has -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +@Immutable +class WordTag( + val word: String, +) : MuteTag { + fun countMemory(): Long = + 1 * pointerSizeInBytes + // 1 fields, 4 bytes each reference (32bit) + word.bytesUsedInMemory() + + override fun toTagArray() = assemble(word) + + override fun toTagIdOnly() = assemble(word) -class RelayTag { companion object { - const val TAG_NAME = "relay" + const val TAG_NAME = "word" @JvmStatic - fun match(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + fun isTagged(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() @JvmStatic - fun notMatch(tag: Array) = tag.has(0) && tag[0] == TAG_NAME + fun isTagged( + tag: Array, + word: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == word @JvmStatic - fun parse(tag: Array): NormalizedRelayUrl? { + fun parse(tag: Array): WordTag? { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - val relay = RelayUrlNormalizer.normalizeOrNull(tag[1]) ?: return null - - return relay + return WordTag(tag[1]) } @JvmStatic - fun assemble(relay: NormalizedRelayUrl) = arrayOf(TAG_NAME, relay.url) + fun parseId(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + @JvmStatic + fun assemble(word: String) = arrayOfNotNull(TAG_NAME, word) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt new file mode 100644 index 0000000000..a0c69898a2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/PeopleListEvent.kt @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.peopleList + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag +import com.vitorpamplona.quartz.nip51Lists.tags.NameTag +import com.vitorpamplona.quartz.utils.TimeUtils +import java.util.UUID +import kotlin.collections.map +import kotlin.collections.plus + +@Immutable +class PeopleListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig), + PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(UserTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(UserTag::parseKey) + + fun name() = tags.firstNotNullOfOrNull(NameTag::parse) + + fun nameOrTitle() = name() ?: title() + + @Deprecated("NIP-51 has deprecated Title. Use name instead", ReplaceWith("name()")) + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) + + fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse) + + fun users() = tags.users() + + fun countMutes() = tags.count(MuteTag::isTagged) + + fun publicPeople(): List = tags.mapNotNull(MuteTag::parse) + + suspend fun privatePeople(signer: NostrSigner): List? = privateTags(signer)?.mapNotNull(MuteTag::parse) + + companion object { + const val KIND = 30000 + const val BLOCK_LIST_D_TAG = "mute" + const val ALT = "List of people" + + fun createBlockAddress(pubKey: HexKey) = Address(KIND, pubKey, BLOCK_LIST_D_TAG) + + fun blockListFor(pubKeyHex: HexKey): String = "30000:$pubKeyHex:$BLOCK_LIST_D_TAG" + + suspend fun create( + name: String, + person: UserTag, + isPrivate: Boolean, + signer: NostrSigner, + dTag: String = UUID.randomUUID().toString(), + createdAt: Long = TimeUtils.now(), + ): PeopleListEvent = + if (isPrivate) { + create( + name = name, + publicPeople = emptyList(), + privatePeople = listOf(person), + signer = signer, + dTag = dTag, + createdAt = createdAt, + ) + } else { + create( + name = name, + publicPeople = listOf(person), + privatePeople = emptyList(), + signer = signer, + dTag = dTag, + createdAt = createdAt, + ) + } + + suspend fun add( + earlierVersion: PeopleListEvent, + person: UserTag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): PeopleListEvent = + if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + resign( + publicTags = earlierVersion.tags, + privateTags = privateTags.plus(person.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } else { + resign( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(person.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun remove( + earlierVersion: PeopleListEvent, + person: MuteTag, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): PeopleListEvent { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + + return resign( + privateTags = privateTags.remove(person.toTagIdOnly()), + publicTags = earlierVersion.tags.remove(person.toTagIdOnly()), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun resign( + publicTags: TagArray, + privateTags: TagArray, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ) = resign( + content = PrivateTagsInContent.encryptNip04(privateTags, signer), + tags = publicTags, + signer = signer, + createdAt = createdAt, + ) + + suspend fun resign( + content: String, + tags: Array>, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): PeopleListEvent { + val newTags = + if (tags.fastAny(AltTag::match)) { + tags + } else { + tags + AltTag.assemble(ALT) + } + + return signer.sign(createdAt, KIND, newTags, content) + } + + suspend fun create( + name: String, + publicPeople: List = emptyList(), + privatePeople: List = emptyList(), + signer: NostrSigner, + dTag: String = UUID.randomUUID().toString(), + createdAt: Long = TimeUtils.now(), + ): PeopleListEvent { + val template = build(name, publicPeople, privatePeople, signer, dTag, createdAt) + return signer.sign(template) + } + + suspend fun build( + name: String, + publicPeople: List = emptyList(), + privatePeople: List = emptyList(), + signer: NostrSigner, + dTag: String = UUID.randomUUID().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip04(privatePeople.map { it.toTagArray() }.toTypedArray(), signer), + createdAt = createdAt, + ) { + dTag(dTag) + alt(ALT) + name(name) + peoples(publicPeople) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..2c9053ab2e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayBuilderExt.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.peopleList + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.tags.NameTag + +fun TagArrayBuilder.name(name: String) = addUnique(NameTag.assemble(name)) + +fun TagArrayBuilder.peoples(peoples: List) = addAll(peoples.map { it.toTagArray() }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayExt.kt new file mode 100644 index 0000000000..67c4568165 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/peopleList/TagArrayExt.kt @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.peopleList + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag +import com.vitorpamplona.quartz.nip51Lists.muteList.tags.WordTag + +fun TagArray.usersAndWords() = mapNotNull(MuteTag::parse) + +fun TagArray.users() = mapNotNull(UserTag::parse) + +fun TagArray.userIds() = mapNotNull(UserTag::parseKey) + +fun TagArray.userIdSet() = mapNotNullTo(mutableSetOf(), UserTag::parseKey) + +fun TagArray.words() = mapNotNull(WordTag::parse) + +fun TagArray.wordSet() = mapNotNullTo(mutableSetOf(), WordTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BlockedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BlockedRelayListEvent.kt new file mode 100644 index 0000000000..4a6a88c4f5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BlockedRelayListEvent.kt @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.relayLists + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.blockedRelays +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relays +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class BlockedRelayListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicRelays() = tags.relays() + + suspend fun privateRelays(signer: NostrSigner) = privateTags(signer)?.relays() + + suspend fun relays(signer: NostrSigner): List = publicRelays() + (privateRelays(signer) ?: emptyList()) + + companion object { + const val KIND = 10006 + val ALT = "Blocked relays from this author" + val ALT_TAG = arrayOf(AltTag.Companion.assemble(ALT)) + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "") + + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null) + + fun createAddressTag(pubKey: HexKey): String = Address.Companion.assemble(KIND, pubKey, "") + + suspend fun updateRelayList( + earlierVersion: BlockedRelayListEvent, + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): BlockedRelayListEvent { + val newRelayList = relays.map { RelayTag.assemble(it) } + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + + val publicTags = earlierVersion.tags.remove(RelayTag::match) + val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) + + return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags) + } + + suspend fun create( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): BlockedRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, ALT_TAG, privateTagArray) + } + + fun create( + relays: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): BlockedRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, ALT_TAG, privateTagArray) + } + + suspend fun build( + publicRelays: List = emptyList(), + privateRelays: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip04(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + blockedRelays(publicRelays) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/TrustedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/TrustedRelayListEvent.kt new file mode 100644 index 0000000000..7fd8262466 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/TrustedRelayListEvent.kt @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.relayLists + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relays +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.trustedRelays +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.plus + +@Immutable +class TrustedRelayListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicRelays() = tags.relays() + + suspend fun decryptPrivateRelays(signer: NostrSigner) = privateTags(signer)?.relays() + + suspend fun decryptRelays(signer: NostrSigner): List = publicRelays() + (decryptPrivateRelays(signer) ?: emptyList()) + + companion object { + const val KIND = 10089 + val ALT = "Trusted relays from this author" + val TAGS = arrayOf(AltTag.assemble(ALT)) + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "") + + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null) + + fun createAddressTag(pubKey: HexKey): String = Address.Companion.assemble(KIND, pubKey, "") + + suspend fun updateRelayList( + earlierVersion: TrustedRelayListEvent, + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): TrustedRelayListEvent { + val newRelayList = relays.map { RelayTag.assemble(it) } + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + + val publicTags = earlierVersion.tags.remove(RelayTag::match) + val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) + + return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags) + } + + suspend fun create( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): TrustedRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun create( + relays: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): TrustedRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun build( + publicRelays: List = emptyList(), + privateRelays: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + trustedRelays(publicRelays) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/RelayTag.kt similarity index 97% rename from quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/tags/RelayTag.kt rename to quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/RelayTag.kt index c69057fd1c..b7c7261cb4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/edits/tags/RelayTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/RelayTag.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.experimental.edits.tags +package com.vitorpamplona.quartz.nip51Lists.relayLists.tags import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..b8626bf5fe --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.relayLists.tags + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent +import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent +import com.vitorpamplona.quartz.nip51Lists.tags.NameTag +import kotlin.collections.map + +fun TagArrayBuilder.name(name: String) = addUnique(NameTag.assemble(name)) + +fun TagArrayBuilder.searchRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) + +fun TagArrayBuilder.blockedRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) + +fun TagArrayBuilder.trustedRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) + +fun TagArrayBuilder.privateRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) + +fun TagArrayBuilder.relaySet(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayExt.kt new file mode 100644 index 0000000000..35413f65be --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayExt.kt @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.relayLists.tags + +import com.vitorpamplona.quartz.nip01Core.core.TagArray + +fun TagArray.relays() = mapNotNull(RelayTag::parse) + +fun TagArray.relaySet() = mapNotNullTo(mutableSetOf(), RelayTag::parse) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relaySets/RelaySetEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relaySets/RelaySetEvent.kt new file mode 100644 index 0000000000..5866237eed --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relaySets/RelaySetEvent.kt @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.relaySets + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relaySet +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag +import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag +import com.vitorpamplona.quartz.nip51Lists.tags.NameTag +import com.vitorpamplona.quartz.nip51Lists.tags.RelayTag +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class RelaySetEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun relays(): List = tags.mapNotNull(RelayTag.Companion::parse) + + fun name() = tags.firstNotNullOfOrNull(NameTag::parse) + + fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse) + + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) + + companion object { + const val KIND = 30002 + const val ALT = "Relay list" + val ALT_TAG = arrayOf(AltTag.assemble(ALT)) + + suspend fun updateRelayList( + earlierVersion: RelaySetEvent, + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): RelaySetEvent { + val newRelayList = relays.map { RelayTag.assemble(it) } + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + + val publicTags = earlierVersion.tags.remove(RelayTag::match) + val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) + + return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags) + } + + suspend fun create( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): RelaySetEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, ALT_TAG, privateTagArray) + } + + suspend fun create( + relays: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): RelaySetEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, ALT_TAG, privateTagArray) + } + + suspend fun build( + publicRelays: List = emptyList(), + privateRelays: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + relaySet(publicRelays) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/TitleTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/TitleTag.kt index f3ebfc5cb2..5687d5fbf5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/TitleTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/tags/TitleTag.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip51Lists.tags -@Deprecated("Use NameTag Instead") class TitleTag { companion object { const val TAG_NAME = "title" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarDateSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarDateSlotEvent.kt index 4418671c27..6e74d196f0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarDateSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarDateSlotEvent.kt @@ -50,13 +50,12 @@ class CalendarDateSlotEvent( const val KIND = 31922 const val ALT = "Full-day calendar event" - fun create( + suspend fun create( signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (CalendarDateSlotEvent) -> Unit, - ) { + ): CalendarDateSlotEvent { val tags = arrayOf(AltTag.assemble(ALT)) - signer.sign(createdAt, KIND, tags, "", onReady) + return signer.sign(createdAt, KIND, tags, "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarEvent.kt index 72b7e9a154..9bec27678c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarEvent.kt @@ -40,13 +40,12 @@ class CalendarEvent( const val KIND = 31924 const val ALT = "Calendar" - fun create( + suspend fun create( signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (CalendarEvent) -> Unit, - ) { + ): CalendarEvent { val tags = arrayOf(AltTag.assemble(ALT)) - signer.sign(createdAt, KIND, tags, "", onReady) + return signer.sign(createdAt, KIND, tags, "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarRSVPEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarRSVPEvent.kt index 37ff201914..700f7a5663 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarRSVPEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarRSVPEvent.kt @@ -52,13 +52,12 @@ class CalendarRSVPEvent( const val KIND = 31925 const val ALT = "Calendar event's invitation response" - fun create( + suspend fun create( signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (CalendarRSVPEvent) -> Unit, - ) { + ): CalendarRSVPEvent { val tags = arrayOf(AltTag.assemble(ALT)) - signer.sign(createdAt, KIND, tags, "", onReady) + return signer.sign(createdAt, KIND, tags, "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarTimeSlotEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarTimeSlotEvent.kt index 9f34f6c1de..2b9b5e2e37 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarTimeSlotEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip52Calendar/CalendarTimeSlotEvent.kt @@ -57,13 +57,12 @@ class CalendarTimeSlotEvent( const val KIND = 31923 const val ALT = "Calendar time-slot event" - fun create( + suspend fun create( signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (CalendarTimeSlotEvent) -> Unit, - ) { + ): CalendarTimeSlotEvent { val tags = arrayOf(AltTag.assemble(ALT)) - signer.sign(createdAt, KIND, tags, "", onReady) + return signer.sign(createdAt, KIND, tags, "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt index 45104fedeb..23c919fa6b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/streaming/LiveActivitiesEvent.kt @@ -124,13 +124,12 @@ class LiveActivitiesEvent( const val KIND = 30311 const val ALT = "Live activity event" - fun create( + suspend fun create( signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (LiveActivitiesEvent) -> Unit, - ) { + ): LiveActivitiesEvent { val tags = arrayOf(AltTag.assemble(ALT)) - signer.sign(createdAt, KIND, tags, "", onReady) + return signer.sign(createdAt, KIND, tags, "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index cc490826a2..f8d65cda9a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -133,21 +133,20 @@ class WikiNoteEvent( companion object { const val KIND = 30818 - fun create( + suspend fun create( msg: String, title: String?, replyTos: List?, mentions: List?, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (WikiNoteEvent) -> Unit, - ) { + ): WikiNoteEvent { val tags = mutableListOf>() replyTos?.forEach { tags.add(arrayOf("e", it)) } mentions?.forEach { tags.add(arrayOf("p", it)) } title?.let { tags.add(arrayOf("title", it)) } tags.add(AltTag.assemble("Wiki Post: $title")) - signer.sign(createdAt, KIND, tags.toTypedArray(), msg, onReady) + return signer.sign(createdAt, KIND, tags.toTypedArray(), msg) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/CommandType.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/CommandType.kt index 6f738db186..4aadd4a8ad 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/CommandType.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/CommandType.kt @@ -33,7 +33,7 @@ enum class CommandType( DERIVE_KEY("derive_key"), ; - companion object Companion { + companion object { fun parse(code: String): CommandType? = when (code) { SIGN_EVENT.code -> SIGN_EVENT diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt index b8018518bd..859c52b35f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/SignerResult.kt @@ -24,38 +24,43 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent -interface RequestAddressed +sealed interface SignerResult { + sealed interface RequestAddressed : SignerResult { + class Successful( + val result: T, + ) : RequestAddressed -sealed class SignerResult { - class Successful( - val result: T, - ) : SignerResult(), - RequestAddressed + class ManuallyRejected : RequestAddressed - class Rejected : - SignerResult(), - RequestAddressed + class AutomaticallyRejected : RequestAddressed - class ReceivedButCouldNotPerform( - message: String? = null, - ) : SignerResult(), - RequestAddressed + class TimedOut : RequestAddressed - class ReceivedButCouldNotParseEventFromResult( - val eventJson: String, - ) : SignerResult(), - RequestAddressed + class NoActivityToLaunchFrom : RequestAddressed - class ReceivedButCouldNotVerifyResultingEvent( - val invalidEvent: Event, - ) : SignerResult(), - RequestAddressed + class SignerNotFound : RequestAddressed - class ErrorExceptionCallingContentResolver( - val e: Exception? = null, - ) : SignerResult() + class ReceivedButCouldNotPerform( + val message: String? = null, + ) : RequestAddressed - class RequiresManualApproval : SignerResult() + class ReceivedButCouldNotParseEventFromResult( + val eventJson: String, + ) : RequestAddressed + + class ReceivedButCouldNotVerifyResultingEvent( + val invalidEvent: Event, + ) : RequestAddressed + } + + sealed interface RequestIncomplete : SignerResult { + class RequiresManualApproval : RequestIncomplete + + class ErrorExceptionCallingContentResolver( + val e: Exception? = null, + ) : SignerResult, + RequestIncomplete + } } interface IResult diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt index 197c639f8b..9e496471ea 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DecryptZapQuery.kt @@ -48,18 +48,18 @@ class DecryptZapQuery( val event = Event.fromJsonOrNull(decryptedEventAsJson) as? LnZapPrivateEvent if (event != null) { if (event.verify()) { - SignerResult.Successful(ZapEventDecryptionResult(event)) + SignerResult.RequestAddressed.Successful(ZapEventDecryptionResult(event)) } else { - SignerResult.ReceivedButCouldNotVerifyResultingEvent(event) + SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent(event) } } else { - SignerResult.ReceivedButCouldNotParseEventFromResult(decryptedEventAsJson) + SignerResult.RequestAddressed.ReceivedButCouldNotParseEventFromResult(decryptedEventAsJson) } } else { - SignerResult.ReceivedButCouldNotPerform(decryptedEventAsJson) + SignerResult.RequestAddressed.ReceivedButCouldNotPerform(decryptedEventAsJson) } } else { - SignerResult.ReceivedButCouldNotPerform(decryptedEventAsJson) + SignerResult.RequestAddressed.ReceivedButCouldNotPerform(decryptedEventAsJson) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DeriveKeyQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DeriveKeyQuery.kt index 6d52621633..e2fe9a4606 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DeriveKeyQuery.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/DeriveKeyQuery.kt @@ -43,9 +43,9 @@ class DeriveKeyQuery( ) { cursor -> val newPrivateKey = cursor.getStringByName("result") if (!newPrivateKey.isNullOrBlank()) { - SignerResult.Successful(DerivationResult(newPrivateKey)) + SignerResult.RequestAddressed.Successful(DerivationResult(newPrivateKey)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/LoginQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/LoginQuery.kt index 9ab83d9687..b86099b2ba 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/LoginQuery.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/LoginQuery.kt @@ -45,9 +45,9 @@ class LoginQuery( ) { cursor -> val pubkeyHex = cursor.getStringByName("result") if (!pubkeyHex.isNullOrBlank()) { - SignerResult.Successful(PubKeyResult(pubkeyHex, packageName)) + SignerResult.RequestAddressed.Successful(PubKeyResult(pubkeyHex, packageName)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04DecryptQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04DecryptQuery.kt index 35bc8cb1da..ed7b13d080 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04DecryptQuery.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04DecryptQuery.kt @@ -46,9 +46,9 @@ class Nip04DecryptQuery( ) { cursor -> val plaintext = cursor.getStringByName("result") if (!plaintext.isNullOrBlank()) { - SignerResult.Successful(DecryptionResult(plaintext)) + SignerResult.RequestAddressed.Successful(DecryptionResult(plaintext)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04EncryptQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04EncryptQuery.kt index 96273ae6ee..a318019f71 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04EncryptQuery.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip04EncryptQuery.kt @@ -46,9 +46,9 @@ class Nip04EncryptQuery( ) { cursor -> val ciphertext = cursor.getStringByName("result") if (!ciphertext.isNullOrBlank()) { - SignerResult.Successful(EncryptionResult(ciphertext)) + SignerResult.RequestAddressed.Successful(EncryptionResult(ciphertext)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44DecryptQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44DecryptQuery.kt index 844143b292..4d9288f74c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44DecryptQuery.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44DecryptQuery.kt @@ -46,9 +46,9 @@ class Nip44DecryptQuery( ) { cursor -> val plaintext = cursor.getStringByName("result") if (!plaintext.isNullOrBlank()) { - SignerResult.Successful(DecryptionResult(plaintext)) + SignerResult.RequestAddressed.Successful(DecryptionResult(plaintext)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44EncryptQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44EncryptQuery.kt index 37deecc04b..6e074344f5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44EncryptQuery.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/Nip44EncryptQuery.kt @@ -46,9 +46,9 @@ class Nip44EncryptQuery( ) { cursor -> val ciphertext = cursor.getStringByName("result") if (!ciphertext.isNullOrBlank()) { - SignerResult.Successful(EncryptionResult(ciphertext)) + SignerResult.RequestAddressed.Successful(EncryptionResult(ciphertext)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/SignQuery.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/SignQuery.kt index b665c667f0..bf1bc9924b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/SignQuery.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/queries/SignQuery.kt @@ -50,20 +50,20 @@ class SignQuery( val event = Event.fromJsonOrNull(eventJson) if (event != null) { if (event.verify()) { - SignerResult.Successful(SignResult(event)) + SignerResult.RequestAddressed.Successful(SignResult(event)) } else { - SignerResult.ReceivedButCouldNotVerifyResultingEvent(event) + SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent(event) } } else { - SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson) + SignerResult.RequestAddressed.ReceivedButCouldNotParseEventFromResult(eventJson) } } else { - SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson) + SignerResult.RequestAddressed.ReceivedButCouldNotParseEventFromResult(eventJson) } } else { val signature = cursor.getStringByName("result") if (!signature.isNullOrBlank()) { - val event = + val event: Event = EventFactory.create( id = unsignedEvent.id, pubKey = unsignedEvent.pubKey, @@ -74,12 +74,12 @@ class SignQuery( sig = signature, ) if (event.verify()) { - SignerResult.Successful(SignResult(event)) + SignerResult.RequestAddressed.Successful(SignResult(event)) } else { - SignerResult.ReceivedButCouldNotVerifyResultingEvent(event) + SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent(event) } } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/ContentResolverExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/ContentResolverExt.kt index 9dcb25e4e5..ae940905b4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/ContentResolverExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/background/utils/ContentResolverExt.kt @@ -42,15 +42,15 @@ fun ContentResolver.query( ).use { if (it != null && it.moveToFirst()) { if (it.getColumnIndex("rejected") > -1) { - SignerResult.Rejected() + SignerResult.RequestAddressed.AutomaticallyRejected() } else { map(it) } } else { - SignerResult.RequiresManualApproval() + SignerResult.RequestIncomplete.RequiresManualApproval() } } } catch (e: Exception) { Log.e("ExternalSignerLauncher", "Failed to query the Signer app in the background", e) - SignerResult.ErrorExceptionCallingContentResolver(e) + SignerResult.RequestIncomplete.ErrorExceptionCallingContentResolver(e) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt deleted file mode 100644 index ea02137a9a..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestDatabase.kt +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground - -import android.content.Intent -import androidx.collection.LruCache -import com.vitorpamplona.quartz.utils.RandomInstance - -class IntentRequestDatabase { - private val awaitingRequests = LruCache(2000) - private var appLauncher: ((Intent) -> Unit)? = null - - /** Call this function when the launcher becomes available on activity, fragment or compose */ - fun registerForegroundLauncher(launcher: ((Intent) -> Unit)) { - this.appLauncher = launcher - } - - /** Call this function when the activity is destroyed or is about to be replaced. */ - fun unregisterForegroundLauncher(launcher: ((Intent) -> Unit)) { - if (this.appLauncher == launcher) { - this.appLauncher = null - } - } - - fun newResponse(data: Intent) { - val callId = data.getStringExtra("id") - if (callId != null) { - awaitingRequests[callId]?.consume(data) - awaitingRequests.remove(callId) - } - } - - fun launch( - requestIntent: Intent, - responseProcessor: NewResultProcessor, - ) { - appLauncher?.let { - val callId = RandomInstance.randomChars(32) - awaitingRequests.put(callId, responseProcessor) - - requestIntent.putExtra("id", callId) - requestIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) - it.invoke(requestIntent) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt new file mode 100644 index 0000000000..8b06b2387e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground + +import android.content.ActivityNotFoundException +import android.content.Intent +import androidx.collection.LruCache +import com.vitorpamplona.quartz.nip55AndroidSigner.api.IResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlinx.coroutines.CancellationException +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume + +/** + * This class manages the lifecycle of foreground signing requests in a NIP-55 compliant Android signer flow. + * + * - It tracks pending signing requests using a unique ID and allows for awaiting their results via coroutines. + * - Provides a way to launch foreground Intents (to request user approval) and wait for the response. + * - Handles timeouts on user approval using [tryAndWait]. + * - Collects results via [newResponse] when the user responds to the foreground request. + * + * Main components: + * + * - `awaitingRequests`: LRU cache mapping request IDs to continuations for async response handling. + * - `appLauncher`: Function reference to launch foreground Intents (typically provided by an Activity). + * - `launchAndWait`: Suspend function to send an Intent, wait for an answer, and parse the result. + * - `newResponse`: Handles incoming results from the foreground activity using a unique ID. + * + * Key usage flows: + * + * - Request initiated -> store continuation by ID -> launch intent -> wait + * - User responds -> resume continuation -> remove ID -> return parsed result + * + * The class also cleans up pending requests on timeout or cancellation. + */ +class IntentRequestManager( + val foregroundApprovalTimeout: Long = 30000, +) { + val activityNotFoundIntent = Intent() + + // LRU cache to store pending requests and their continuations. + private val awaitingRequests = LruCache>(2000) + + // Function to launch an Intent in the foreground. + private var appLauncher: ((Intent) -> Unit)? = null + + /** Call this function when the launcher becomes available on activity, fragment or compose */ + fun registerForegroundLauncher(launcher: ((Intent) -> Unit)) { + this.appLauncher = launcher + } + + /** Call this function when the activity is destroyed or is about to be replaced. */ + fun unregisterForegroundLauncher(launcher: ((Intent) -> Unit)) { + if (this.appLauncher == launcher) { + this.appLauncher = null + } + } + + fun newResponse(data: Intent) { + val callId = data.getStringExtra("id") + if (callId != null) { + awaitingRequests[callId]?.resume(data) + awaitingRequests.remove(callId) + } + } + + fun hasForegroundActivity() = appLauncher != null + + /** + * Launches the signer, waits and parses the result + * + * @param requestIntent The Intent to be launched. + * @param parser A function that parses the response Intent into a [SignerResult.RequestAddressed]. + * @return The result after parsing the Intent using the provided parser. + * + * This function uses the [tryAndWait] utility to implement a timeout on the foreground approval. + * It assigns a unique ID to the request and keeps a continuation to resume once the result is received. + * If the timeout occurs or the continuation is cancelled, the request ID is cleaned up from [awaitingRequests]. + * Flags are added to the Intent to ensure it is brought to the front if already running. + */ + suspend fun launchWaitAndParse( + requestIntentBuilder: () -> Intent, + parser: (intent: Intent) -> SignerResult.RequestAddressed, + ): SignerResult.RequestAddressed = + appLauncher?.let { launcher -> + val requestIntent = requestIntentBuilder() + val callId = RandomInstance.randomChars(32) + + requestIntent.putExtra("id", callId) + requestIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) + + val resultIntent = + tryAndWait(foregroundApprovalTimeout) { continuation -> + continuation.invokeOnCancellation { + awaitingRequests.remove(callId) + } + + awaitingRequests.put(callId, continuation) + + try { + launcher.invoke(requestIntent) + } catch (_: ActivityNotFoundException) { + awaitingRequests.remove(callId) + continuation.resume(activityNotFoundIntent) + } catch (e: Exception) { + awaitingRequests.remove(callId) + continuation.resume(null) + if (e is CancellationException) throw e + } + } + + when (resultIntent) { + null -> SignerResult.RequestAddressed.TimedOut() + activityNotFoundIntent -> SignerResult.RequestAddressed.SignerNotFound() + else -> parser(resultIntent) + } + } ?: SignerResult.RequestAddressed.NoActivityToLaunchFrom() +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt index 4c7fa632b4..0d602809bf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt @@ -34,21 +34,21 @@ class DecryptZapResponse { return intent } - fun parse(intent: Intent): SignerResult { + fun parse(intent: Intent): SignerResult.RequestAddressed { val eventJson = intent.getStringExtra("result") return if (!eventJson.isNullOrBlank()) { if (eventJson.startsWith("{")) { val event = Event.fromJsonOrNull(eventJson) as? LnZapPrivateEvent if (event != null) { - SignerResult.Successful(ZapEventDecryptionResult(event)) + SignerResult.RequestAddressed.Successful(ZapEventDecryptionResult(event)) } else { - SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson) + SignerResult.RequestAddressed.ReceivedButCouldNotParseEventFromResult(eventJson) } } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt index 8288371398..8a4a3085ce 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt @@ -26,12 +26,12 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult class DeriveKeyResponse { companion object { - fun parse(intent: Intent): SignerResult { + fun parse(intent: Intent): SignerResult.RequestAddressed { val newPrivateKey = intent.getStringExtra("result") return if (newPrivateKey != null) { - SignerResult.Successful(DerivationResult(newPrivateKey)) + SignerResult.RequestAddressed.Successful(DerivationResult(newPrivateKey)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt index 27c3901e8d..1a5cca5560 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt @@ -26,19 +26,19 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult class LoginResponse { companion object { - fun parse(intent: Intent): SignerResult { + fun parse(intent: Intent): SignerResult.RequestAddressed { val pubkey = intent.getStringExtra("result") val packageName = intent.getStringExtra("package") return if (pubkey != null && packageName != null) { - SignerResult.Successful( + SignerResult.RequestAddressed.Successful( PubKeyResult( pubkey = pubkey, packageName = packageName, ), ) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt index accae93b25..c7e78b366b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt @@ -26,12 +26,12 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult class Nip04DecryptResponse { companion object { - fun parse(intent: Intent): SignerResult { + fun parse(intent: Intent): SignerResult.RequestAddressed { val ciphertext = intent.getStringExtra("result") return if (ciphertext != null) { - SignerResult.Successful(DecryptionResult(ciphertext)) + SignerResult.RequestAddressed.Successful(DecryptionResult(ciphertext)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt index dc7e167d6f..d16ea4d7b7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt @@ -26,12 +26,12 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult class Nip04EncryptResponse { companion object { - fun parse(intent: Intent): SignerResult { + fun parse(intent: Intent): SignerResult.RequestAddressed { val ciphertext = intent.getStringExtra("result") return if (ciphertext != null) { - SignerResult.Successful(EncryptionResult(ciphertext)) + SignerResult.RequestAddressed.Successful(EncryptionResult(ciphertext)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt index 9d63a6899d..f320afb79b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt @@ -26,12 +26,12 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult class Nip44DecryptResponse { companion object { - fun parse(intent: Intent): SignerResult { + fun parse(intent: Intent): SignerResult.RequestAddressed { val ciphertext = intent.getStringExtra("result") return if (ciphertext != null) { - SignerResult.Successful(DecryptionResult(ciphertext)) + SignerResult.RequestAddressed.Successful(DecryptionResult(ciphertext)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt index 180a81f6e1..c9a9412bda 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt @@ -26,12 +26,12 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult class Nip44EncryptResponse { companion object { - fun parse(intent: Intent): SignerResult { + fun parse(intent: Intent): SignerResult.RequestAddressed { val ciphertext = intent.getStringExtra("result") return if (ciphertext != null) { - SignerResult.Successful(EncryptionResult(ciphertext)) + SignerResult.RequestAddressed.Successful(EncryptionResult(ciphertext)) } else { - SignerResult.ReceivedButCouldNotPerform() + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt index 88a13b8c09..25d040ca46 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt @@ -32,27 +32,27 @@ class SignResponse { fun parse( intent: Intent, unsignedEvent: Event, - ): SignerResult { + ): SignerResult.RequestAddressed { val eventJson = intent.getStringExtra("event") return if (eventJson != null) { if (eventJson.startsWith("{")) { val event = Event.fromJsonOrNull(eventJson) if (event != null) { if (event.verify()) { - SignerResult.Successful(SignResult(event)) + SignerResult.RequestAddressed.Successful(SignResult(event)) } else { - SignerResult.ReceivedButCouldNotVerifyResultingEvent(event) + SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent(event) } } else { - SignerResult.ReceivedButCouldNotParseEventFromResult(eventJson) + SignerResult.RequestAddressed.ReceivedButCouldNotParseEventFromResult(eventJson) } } else { - SignerResult.ReceivedButCouldNotPerform(eventJson) + SignerResult.RequestAddressed.ReceivedButCouldNotPerform(eventJson) } } else { val signature = intent.getStringExtra("result") if (signature != null && signature.length == 128) { - val event = + val event: Event = EventFactory.create( id = unsignedEvent.id, pubKey = unsignedEvent.pubKey, @@ -63,12 +63,12 @@ class SignResponse { sig = signature, ) if (event.verify()) { - SignerResult.Successful(SignResult(event)) + SignerResult.RequestAddressed.Successful(SignResult(event)) } else { - SignerResult.ReceivedButCouldNotVerifyResultingEvent(event) + SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent(event) } } else { - SignerResult.ReceivedButCouldNotPerform(signature) + SignerResult.RequestAddressed.ReceivedButCouldNotPerform(signature) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt deleted file mode 100644 index 26c4368807..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/DeriveKeyResultProcessor.kt +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors - -import android.content.Intent -import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DeriveKeyResponse - -class DeriveKeyResultProcessor( - val onReady: (String) -> Unit, -) : NewResultProcessor { - override fun consume(intent: Intent) { - val foregroundResult = DeriveKeyResponse.parse(intent) - - if (foregroundResult is SignerResult.Successful) { - onReady(foregroundResult.result.newPrivKey) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt deleted file mode 100644 index b38fd62ddb..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04DecryptResultProcessor.kt +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors - -import android.content.Intent -import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip04DecryptResponse - -class Nip04DecryptResultProcessor( - val onReady: (String) -> Unit, -) : NewResultProcessor { - override fun consume(intent: Intent) { - val foregroundResult = Nip04DecryptResponse.parse(intent) - - if (foregroundResult is SignerResult.Successful) { - onReady(foregroundResult.result.plaintext) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt deleted file mode 100644 index afcc122580..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip04EncryptResultProcessor.kt +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors - -import android.content.Intent -import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip04EncryptResponse - -class Nip04EncryptResultProcessor( - val onReady: (String) -> Unit, -) : NewResultProcessor { - override fun consume(intent: Intent) { - val foregroundResult = Nip04EncryptResponse.parse(intent) - - if (foregroundResult is SignerResult.Successful) { - onReady(foregroundResult.result.ciphertext) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt deleted file mode 100644 index 871ca8b948..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44DecryptResultProcessor.kt +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors - -import android.content.Intent -import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44DecryptResponse - -class Nip44DecryptResultProcessor( - val onReady: (String) -> Unit, -) : NewResultProcessor { - override fun consume(intent: Intent) { - val foregroundResult = Nip44DecryptResponse.parse(intent) - - if (foregroundResult is SignerResult.Successful) { - onReady(foregroundResult.result.plaintext) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt deleted file mode 100644 index eecb92155f..0000000000 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/processors/Nip44EncryptResultProcessor.kt +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors - -import android.content.Intent -import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.NewResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44EncryptResponse - -class Nip44EncryptResultProcessor( - val onReady: (String) -> Unit, -) : NewResultProcessor { - override fun consume(intent: Intent) { - val foregroundResult = Nip44EncryptResponse.parse(intent) - if (foregroundResult is SignerResult.Successful) { - onReady(foregroundResult.result.ciphertext) - } - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IActivityLauncher.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IActivityLauncher.kt index af3f02ade2..5ac3350a8e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IActivityLauncher.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/IActivityLauncher.kt @@ -28,4 +28,6 @@ interface IActivityLauncher { fun unregisterForegroundLauncher(launcher: ((Intent) -> Unit)) fun newResponse(data: Intent) + + fun hasForegroundActivity(): Boolean } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt index f1ffb4fbd4..f6d23f26d1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt @@ -26,10 +26,18 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers.BackgroundRequestHandler import com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers.ForegroundRequestHandler import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import java.lang.Exception class NostrSignerExternal( pubKey: HexKey, @@ -54,13 +62,14 @@ class NostrSignerExternal( this.foregroundQuery.launcher.newResponse(data) } - override fun sign( + override fun hasForegroundActivity() = this.foregroundQuery.launcher.hasForegroundActivity() + + override suspend fun sign( createdAt: Long, kind: Int, tags: Array>, content: String, - onReady: (T) -> Unit, - ) { + ): T { val unsignedEvent = Event( id = EventHasher.hashId(pubKey, createdAt, kind, tags, content), @@ -72,70 +81,110 @@ class NostrSignerExternal( sig = "", ) - val newOnReady: (Event) -> Unit = { result -> - (result as? T)?.let(onReady) + val result = backgroundQuery.sign(unsignedEvent) ?: foregroundQuery.sign(unsignedEvent) + + if (result is SignerResult.RequestAddressed.Successful) { + (result.result.event as? T)?.let { + return it + } } - if (!backgroundQuery.sign(unsignedEvent, newOnReady)) { - foregroundQuery.sign(unsignedEvent, newOnReady) - } + throw convertExceptions("Could not sign", result) } - override fun nip04Encrypt( + override suspend fun nip04Encrypt( plaintext: String, toPublicKey: HexKey, - onReady: (String) -> Unit, - ) { - if (!backgroundQuery.nip04Encrypt(plaintext, toPublicKey, onReady)) { - foregroundQuery.nip04Encrypt(plaintext, toPublicKey, onReady) + ): String { + val result = backgroundQuery.nip04Encrypt(plaintext, toPublicKey) ?: foregroundQuery.nip04Encrypt(plaintext, toPublicKey) + + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.ciphertext } + + throw convertExceptions("Could not encrypt", result) } - override fun nip04Decrypt( + override suspend fun nip04Decrypt( ciphertext: String, fromPublicKey: HexKey, - onReady: (String) -> Unit, - ) { - if (!backgroundQuery.nip04Decrypt(ciphertext, fromPublicKey, onReady)) { - foregroundQuery.nip04Decrypt(ciphertext, fromPublicKey, onReady) + ): String { + if (ciphertext.isBlank()) throw SignerExceptions.NothingToDecrypt() + + val result = backgroundQuery.nip04Decrypt(ciphertext, fromPublicKey) ?: foregroundQuery.nip04Decrypt(ciphertext, fromPublicKey) + + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.plaintext } + + throw convertExceptions("Could not decrypt", result) } - override fun nip44Encrypt( + override suspend fun nip44Encrypt( plaintext: String, toPublicKey: HexKey, - onReady: (String) -> Unit, - ) { - if (!backgroundQuery.nip44Encrypt(plaintext, toPublicKey, onReady)) { - foregroundQuery.nip44Encrypt(plaintext, toPublicKey, onReady) + ): String { + val result = backgroundQuery.nip44Encrypt(plaintext, toPublicKey) ?: foregroundQuery.nip44Encrypt(plaintext, toPublicKey) + + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.ciphertext } + + throw convertExceptions("Could not encrypt", result) } - override fun nip44Decrypt( + override suspend fun nip44Decrypt( ciphertext: String, fromPublicKey: HexKey, - onReady: (String) -> Unit, - ) { - if (!backgroundQuery.nip44Decrypt(ciphertext, fromPublicKey, onReady)) { - foregroundQuery.nip44Decrypt(ciphertext, fromPublicKey, onReady) + ): String { + if (ciphertext.isBlank()) throw SignerExceptions.NothingToDecrypt() + + val result = backgroundQuery.nip44Decrypt(ciphertext, fromPublicKey) ?: foregroundQuery.nip44Decrypt(ciphertext, fromPublicKey) + + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.plaintext } + + throw convertExceptions("Could not decrypt", result) } - override fun deriveKey( - nonce: HexKey, - onReady: (HexKey) -> Unit, - ) { - if (!backgroundQuery.deriveKey(nonce, onReady)) { - foregroundQuery.deriveKey(nonce, onReady) + override suspend fun deriveKey(nonce: HexKey): HexKey { + val result = backgroundQuery.deriveKey(nonce) ?: foregroundQuery.deriveKey(nonce) + + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.newPrivKey } + + throw convertExceptions("Could not derive key", result) } - override fun decryptZapEvent( - event: LnZapRequestEvent, - onReady: (LnZapPrivateEvent) -> Unit, - ) { - if (!backgroundQuery.decryptZapEvent(event, onReady)) { - foregroundQuery.decryptZapEvent(event, onReady) + override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent { + if (!event.isPrivateZap()) throw SignerExceptions.NothingToDecrypt() + + val result = backgroundQuery.decryptZapEvent(event) ?: foregroundQuery.decryptZapEvent(event) + + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.privateEvent } + + throw convertExceptions("Could not decrypt private zap", result) } + + fun convertExceptions( + title: String, + result: SignerResult.RequestAddressed<*>, + ): Exception = + when (result) { + is SignerResult.RequestAddressed.Successful<*> -> IllegalStateException("$title: This should not happen. There is a bug on Quartz.") + is SignerResult.RequestAddressed.ReceivedButCouldNotParseEventFromResult<*> -> IllegalStateException("$title: Failed to parse event: ${result.eventJson}.") + is SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent<*> -> IllegalStateException("$title: Failed to verify event: ${result.invalidEvent.toJson()}.") + + is SignerResult.RequestAddressed.ReceivedButCouldNotPerform<*> -> SignerExceptions.CouldNotPerformException("$title: ${result.message}") + is SignerResult.RequestAddressed.SignerNotFound<*> -> SignerExceptions.SignerNotFoundException("$title: Signer app was not found.") + + is SignerResult.RequestAddressed.AutomaticallyRejected<*> -> SignerExceptions.AutomaticallyUnauthorizedException("$title: User has rejected the request.") + is SignerResult.RequestAddressed.ManuallyRejected<*> -> SignerExceptions.ManuallyUnauthorizedException("$title: User has rejected the request.") + is SignerResult.RequestAddressed.TimedOut<*> -> SignerExceptions.TimedOutException("$title: User didn't accept or reject in time.") + is SignerResult.RequestAddressed.NoActivityToLaunchFrom<*> -> SignerExceptions.RunningOnBackgroundWithoutAutomaticPermissionException("$title: No activity to launch from.") + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt index 9edea8f543..fcf072db51 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/BackgroundRequestHandler.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult -import com.vitorpamplona.quartz.nip55AndroidSigner.api.RequestAddressed import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult @@ -39,7 +38,6 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip04E import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44DecryptQuery import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44EncryptQuery import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.SignQuery -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent class BackgroundRequestHandler( @@ -56,108 +54,31 @@ class BackgroundRequestHandler( val decryptZap = DecryptZapQuery(loggedInUser, packageName, contentResolver) val deriveKey = DeriveKeyQuery(loggedInUser, packageName, contentResolver) - fun login(onReady: (HexKey) -> Unit): Boolean { - val backgroundResult = login.query() + fun login() = login.query() as? SignerResult.RequestAddressed - if (backgroundResult is SignerResult.Successful) { - onReady(backgroundResult.result.pubkey) - } - - return backgroundResult is RequestAddressed - } - - fun sign( - unsignedEvent: Event, - onReady: (Event) -> Unit, - ): Boolean { - val backgroundResult = sign.query(unsignedEvent) - - if (backgroundResult is SignerResult.Successful) { - onReady(backgroundResult.result.event) - } - - return backgroundResult is RequestAddressed - } + fun sign(unsignedEvent: Event) = sign.query(unsignedEvent) as? SignerResult.RequestAddressed fun nip04Encrypt( plaintext: String, toPubKey: HexKey, - onReady: (String) -> Unit, - ): Boolean { - val backgroundResult = nip04Encrypt.query(plaintext, toPubKey) - - if (backgroundResult is SignerResult.Successful) { - onReady(backgroundResult.result.ciphertext) - } - - return backgroundResult is RequestAddressed - } + ) = nip04Encrypt.query(plaintext, toPubKey) as? SignerResult.RequestAddressed fun nip04Decrypt( ciphertext: String, fromPubKey: HexKey, - onReady: (String) -> Unit, - ): Boolean { - val backgroundResult = nip04Decrypt.query(ciphertext, fromPubKey) - - if (backgroundResult is SignerResult.Successful) { - onReady(backgroundResult.result.plaintext) - } - - return backgroundResult is RequestAddressed - } + ) = nip04Decrypt.query(ciphertext, fromPubKey) as? SignerResult.RequestAddressed fun nip44Encrypt( plaintext: String, toPubKey: HexKey, - onReady: (String) -> Unit, - ): Boolean { - val backgroundResult = nip44Encrypt.query(plaintext, toPubKey) - - if (backgroundResult is SignerResult.Successful) { - onReady(backgroundResult.result.ciphertext) - } - - return backgroundResult is RequestAddressed - } + ) = nip44Encrypt.query(plaintext, toPubKey) as? SignerResult.RequestAddressed fun nip44Decrypt( ciphertext: String, fromPubKey: HexKey, - onReady: (String) -> Unit, - ): Boolean { - val backgroundResult = nip44Decrypt.query(ciphertext, fromPubKey) + ) = nip44Decrypt.query(ciphertext, fromPubKey) as? SignerResult.RequestAddressed - if (backgroundResult is SignerResult.Successful) { - onReady(backgroundResult.result.plaintext) - } + fun decryptZapEvent(event: LnZapRequestEvent) = decryptZap.query(event) as? SignerResult.RequestAddressed - return backgroundResult is RequestAddressed - } - - fun decryptZapEvent( - event: LnZapRequestEvent, - onReady: (LnZapPrivateEvent) -> Unit, - ): Boolean { - val backgroundResult = decryptZap.query(event) - - if (backgroundResult is SignerResult.Successful) { - onReady(backgroundResult.result.privateEvent) - } - - return backgroundResult is RequestAddressed - } - - fun deriveKey( - nonce: HexKey, - onReady: (HexKey) -> Unit, - ): Boolean { - val backgroundResult = deriveKey.query(nonce) - - if (backgroundResult is SignerResult.Successful) { - onReady(backgroundResult.result.newPrivKey) - } - - return backgroundResult is RequestAddressed - } + fun deriveKey(nonce: HexKey) = deriveKey.query(nonce) as? SignerResult.RequestAddressed } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt index 27b1584d28..c86f9ebcfc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/handlers/ForegroundRequestHandler.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip55AndroidSigner.client.handlers import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.IntentRequestDatabase +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.IntentRequestManager import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.DecryptZapRequest import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.DeriveKeyRequest import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip04DecryptRequest @@ -30,79 +30,69 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.reques import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip44DecryptRequest import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip44EncryptRequest import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.SignRequest -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.DecryptZapResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.DeriveKeyResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip04DecryptResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip04EncryptResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip44DecryptResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.Nip44EncryptResultProcessor -import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.processors.SignResultProcessor -import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DecryptZapResponse +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DeriveKeyResponse +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip04DecryptResponse +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip04EncryptResponse +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44DecryptResponse +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44EncryptResponse +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.SignResponse import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent class ForegroundRequestHandler( val loggedInUser: HexKey, val packageName: String, + foregroundApprovalTimeout: Long = 30000, ) { - val launcher = IntentRequestDatabase() + val launcher = IntentRequestManager(foregroundApprovalTimeout) - fun sign( - unsignedEvent: Event, - onReady: (Event) -> Unit, - ) = launcher.launch( - SignRequest.assemble(unsignedEvent, loggedInUser, packageName), - SignResultProcessor(unsignedEvent, onReady), - ) + suspend fun sign(unsignedEvent: Event) = + launcher.launchWaitAndParse( + requestIntentBuilder = { SignRequest.assemble(unsignedEvent, loggedInUser, packageName) }, + parser = { intent -> SignResponse.parse(intent, unsignedEvent) }, + ) - fun nip04Encrypt( + suspend fun nip04Encrypt( plaintext: String, toPubKey: HexKey, - onReady: (String) -> Unit, - ) = launcher.launch( - Nip04EncryptRequest.assemble(plaintext, toPubKey, loggedInUser, packageName), - Nip04EncryptResultProcessor(onReady), + ) = launcher.launchWaitAndParse( + requestIntentBuilder = { Nip04EncryptRequest.assemble(plaintext, toPubKey, loggedInUser, packageName) }, + parser = Nip04EncryptResponse::parse, ) - fun nip04Decrypt( + suspend fun nip04Decrypt( ciphertext: String, fromPubKey: HexKey, - onReady: (String) -> Unit, - ) = launcher.launch( - Nip04DecryptRequest.assemble(ciphertext, fromPubKey, loggedInUser, packageName), - Nip04DecryptResultProcessor(onReady), + ) = launcher.launchWaitAndParse( + requestIntentBuilder = { Nip04DecryptRequest.assemble(ciphertext, fromPubKey, loggedInUser, packageName) }, + parser = Nip04DecryptResponse::parse, ) - fun nip44Encrypt( + suspend fun nip44Encrypt( plaintext: String, toPubKey: HexKey, - onReady: (String) -> Unit, - ) = launcher.launch( - Nip44EncryptRequest.assemble(plaintext, toPubKey, loggedInUser, packageName), - Nip44EncryptResultProcessor(onReady), + ) = launcher.launchWaitAndParse( + requestIntentBuilder = { Nip44EncryptRequest.assemble(plaintext, toPubKey, loggedInUser, packageName) }, + parser = Nip44EncryptResponse::parse, ) - fun nip44Decrypt( + suspend fun nip44Decrypt( ciphertext: String, fromPubKey: HexKey, - onReady: (String) -> Unit, - ) = launcher.launch( - Nip44DecryptRequest.assemble(ciphertext, fromPubKey, loggedInUser, packageName), - Nip44DecryptResultProcessor(onReady), + ) = launcher.launchWaitAndParse( + requestIntentBuilder = { Nip44DecryptRequest.assemble(ciphertext, fromPubKey, loggedInUser, packageName) }, + parser = Nip44DecryptResponse::parse, ) - fun decryptZapEvent( - event: LnZapRequestEvent, - onReady: (LnZapPrivateEvent) -> Unit, - ) = launcher.launch( - DecryptZapRequest.assemble(event, loggedInUser, packageName), - DecryptZapResultProcessor(onReady), - ) + suspend fun decryptZapEvent(event: LnZapRequestEvent) = + launcher.launchWaitAndParse( + requestIntentBuilder = { DecryptZapRequest.assemble(event, loggedInUser, packageName) }, + parser = DecryptZapResponse::parse, + ) - fun deriveKey( - nonce: HexKey, - onReady: (HexKey) -> Unit, - ) = launcher.launch( - DeriveKeyRequest.assemble(nonce, loggedInUser, packageName), - DeriveKeyResultProcessor(onReady), - ) + suspend fun deriveKey(nonce: HexKey) = + launcher.launchWaitAndParse( + requestIntentBuilder = { DeriveKeyRequest.assemble(nonce, loggedInUser, packageName) }, + parser = DeriveKeyResponse::parse, + ) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt index 099ea723a7..9b6f5a3637 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt @@ -24,9 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate -import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip56Reports.tags.DefaultReportTag import com.vitorpamplona.quartz.nip56Reports.tags.ReportedAddressTag @@ -75,28 +73,6 @@ class ReportEvent( const val KIND = 1984 const val ALT_PREFIX = "Report for " - fun create( - reportedPost: Event, - type: ReportType, - signer: NostrSigner, - content: String = "", - createdAt: Long = TimeUtils.now(), - onReady: (ReportEvent) -> Unit, - ) { - val reportPostTag = arrayOf("e", reportedPost.id, type.name.lowercase()) - val reportAuthorTag = arrayOf("p", reportedPost.pubKey, type.name.lowercase()) - - var tags: Array> = arrayOf(reportPostTag, reportAuthorTag) - - if (reportedPost is AddressableEvent) { - tags += listOf(arrayOf("a", reportedPost.aTag().toTag())) - } - - tags += listOf(AltTag.assemble("Report for ${type.name}")) - - signer.sign(createdAt, KIND, tags, content, onReady) - } - fun build( reportedPost: Event, type: ReportType, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt index efb16f01cf..f2901533ee 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapPrivateEvent.kt @@ -61,21 +61,18 @@ class LnZapPrivateEvent( const val KIND = 9733 const val ALT = "Private zap" - fun create( + suspend fun create( signer: NostrSigner, tags: Array> = emptyArray(), content: String = "", createdAt: Long = TimeUtils.now(), - onReady: (LnZapPrivateEvent) -> Unit, - ) { - signer.sign(createdAt, KIND, tags, content, onReady) - } + ): LnZapPrivateEvent = signer.sign(createdAt, KIND, tags, content) fun create( signer: NostrSignerSync, tags: Array> = emptyArray(), content: String = "", createdAt: Long = TimeUtils.now(), - ): LnZapPrivateEvent? = signer.sign(createdAt, KIND, tags, content) + ): LnZapPrivateEvent = signer.sign(createdAt, KIND, tags, content) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt index f2e00897ea..48c937658d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.quartz.experimental.zapPolls.tags.PollOptionTag import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider @@ -38,7 +37,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class LnZapRequestEvent( @@ -64,55 +62,23 @@ class LnZapRequestEvent( override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) - // TODO: Create a per key map with resulting options to account for rejections and avoiding reasking. - @Transient - private var privateZapEvent: LnZapPrivateEvent? = null - - override fun countMemory(): Long = super.countMemory() + pointerSizeInBytes + (privateZapEvent?.countMemory() ?: 0) - fun zappedPost() = tags.mapNotNull(ETag::parseId) fun zappedAuthor() = tags.mapNotNull(PTag::parseKey) fun isPrivateZap() = tags.any { t -> t.size >= 2 && t[0] == "anon" && t[1].isNotBlank() } - fun getPrivateZapEvent( - loggedInUserPrivKey: ByteArray, - pubKey: HexKey, - ): LnZapPrivateEvent? { + fun getAnonTag(): String { val anonTag = tags.firstOrNull { t -> t.size >= 2 && t[0] == "anon" } if (anonTag != null) { - val encnote = anonTag[1] - if (encnote.isNotBlank()) { - try { - val note = PrivateZapEncryption.decryptPrivateZapMessage(encnote, loggedInUserPrivKey, pubKey.hexToByteArray()) - val decryptedEvent = fromJson(note) - if (decryptedEvent.kind == 9733) { - return decryptedEvent as LnZapPrivateEvent - } - } catch (e: Exception) { - e.printStackTrace() - } + val encNote = anonTag[1] + if (encNote.isNotBlank()) { + return encNote + } else { + throw IllegalStateException("Anon tag is empty.") } - } - return null - } - - fun cachedPrivateZap(): LnZapPrivateEvent? = privateZapEvent - - fun decryptPrivateZap( - signer: NostrSigner, - onReady: (LnZapPrivateEvent) -> Unit, - ) { - privateZapEvent?.let { - onReady(it) - return - } - - signer.decryptZapEvent(this) { - // caches it - privateZapEvent = it - onReady(it) + } else { + throw IllegalStateException("This is not a private zap.") } } @@ -120,7 +86,7 @@ class LnZapRequestEvent( const val KIND = 9734 const val ALT = "Zap request" - fun create( + suspend fun create( originalNote: Event, relays: Set, signer: NostrSigner, @@ -129,10 +95,7 @@ class LnZapRequestEvent( zapType: LnZapEvent.ZapType, toUserPubHex: String?, createdAt: Long = TimeUtils.now(), - onReady: (LnZapRequestEvent) -> Unit, - ) { - if (zapType == LnZapEvent.ZapType.NONZAP) return - + ): LnZapRequestEvent { var tags = listOf( arrayOf("e", originalNote.id), @@ -147,42 +110,45 @@ class LnZapRequestEvent( tags = tags + listOf(arrayOf(PollOptionTag.TAG_NAME, pollOption.toString())) } - if (zapType == LnZapEvent.ZapType.ANONYMOUS) { - tags = tags + listOf(arrayOf("anon")) - NostrSignerInternal(KeyPair()).sign(createdAt, KIND, tags.toTypedArray(), message, onReady) - } else if (zapType == LnZapEvent.ZapType.PRIVATE) { - tags = tags + listOf(arrayOf("anon", "")) - signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady) - } else { - signer.sign(createdAt, KIND, tags.toTypedArray(), message, onReady) + return when (zapType) { + LnZapEvent.ZapType.PUBLIC -> signer.sign(createdAt, KIND, tags.toTypedArray(), message) + LnZapEvent.ZapType.ANONYMOUS -> { + tags = tags + listOf(arrayOf("anon")) + NostrSignerInternal(KeyPair()).sign(createdAt, KIND, tags.toTypedArray(), message) + } + LnZapEvent.ZapType.PRIVATE -> { + tags = tags + listOf(arrayOf("anon", "")) + signer.sign(createdAt, KIND, tags.toTypedArray(), message) + } + LnZapEvent.ZapType.NONZAP -> throw IllegalArgumentException("Invalid zap type") } } - fun create( + suspend fun create( userHex: String, relays: Set, signer: NostrSigner, message: String, zapType: LnZapEvent.ZapType, createdAt: Long = TimeUtils.now(), - onReady: (LnZapRequestEvent) -> Unit, - ) { - if (zapType == LnZapEvent.ZapType.NONZAP) return - + ): LnZapRequestEvent { var tags = arrayOf( arrayOf("p", userHex), arrayOf("relays") + relays.map { it.url }, ) - if (zapType == LnZapEvent.ZapType.ANONYMOUS) { - tags += arrayOf(arrayOf("anon", "")) - NostrSignerInternal(KeyPair()).sign(createdAt, KIND, tags, message, onReady) - } else if (zapType == LnZapEvent.ZapType.PRIVATE) { - tags += arrayOf(arrayOf("anon", "")) - signer.sign(createdAt, KIND, tags, message, onReady) - } else { - signer.sign(createdAt, KIND, tags, message, onReady) + return when (zapType) { + LnZapEvent.ZapType.PUBLIC -> signer.sign(createdAt, KIND, tags, message) + LnZapEvent.ZapType.ANONYMOUS -> { + tags += arrayOf(arrayOf("anon", "")) + NostrSignerInternal(KeyPair()).sign(createdAt, KIND, tags, message) + } + LnZapEvent.ZapType.PRIVATE -> { + tags += arrayOf(arrayOf("anon", "")) + signer.sign(createdAt, KIND, tags, message) + } + LnZapEvent.ZapType.NONZAP -> throw IllegalArgumentException("Invalid zap type") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt new file mode 100644 index 0000000000..9e8ede2061 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapCache.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip57Zaps + +import androidx.collection.LruCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.caches.DecryptCache + +class PrivateZapCache( + signer: NostrSigner, +) { + private val decryptionCache = + object : LruCache(1000) { + override fun create(key: LnZapRequestEvent): PrivateZapDecryptCache? { + val zappedAuthor = key.zappedAuthor().firstOrNull() + return if (key.isPrivateZap() && zappedAuthor != null) { + PrivateZapDecryptCache(signer) + } else { + null + } + } + } + + fun delete(event: LnZapRequestEvent) { + decryptionCache.remove(event) + } + + fun cachedPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent? = decryptionCache[event]?.cached() + + suspend fun decryptPrivateZap(event: LnZapRequestEvent) = decryptionCache[event]?.decrypt(event) +} + +class PrivateZapDecryptCache( + signer: NostrSigner, +) : DecryptCache(signer) { + override suspend fun decryptAndParse( + event: LnZapRequestEvent, + signer: NostrSigner, + ): LnZapPrivateEvent = signer.decryptZapEvent(event) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt index c0aca3c030..332f8031c1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt @@ -20,12 +20,14 @@ */ package com.vitorpamplona.quartz.nip57Zaps -import android.util.Log +import com.vitorpamplona.quartz.nip01Core.core.Event.Companion.fromJson +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions class PrivateZapRequestBuilder { fun signPrivateZapRequest( @@ -34,11 +36,13 @@ class PrivateZapRequestBuilder { tags: Array>, content: String, signer: NostrSignerSync, - ): T? { - if (signer.keyPair.privKey == null) return null + ): T { + if (signer.keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() val zappedEvent = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.let { it[1] } - val userHex = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] } ?: return null + val userHex = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.let { it[1] } + + require(userHex != null) { "A user is required when creating private zaps" } // if it is a Zap for an Event, use event.id if not, use the user's pubkey val idToGeneratePrivateKey = zappedEvent ?: userHex @@ -52,7 +56,7 @@ class PrivateZapRequestBuilder { val fullTagsNoAnon = tags.filter { t -> t.getOrNull(0) != "anon" }.toTypedArray() - val privateEvent = LnZapPrivateEvent.create(signer, fullTagsNoAnon, content) ?: return null + val privateEvent = LnZapPrivateEvent.create(signer, fullTagsNoAnon, content) val noteJson = privateEvent.toJson() val encryptedContent = @@ -71,22 +75,19 @@ class PrivateZapRequestBuilder { fun decryptZapEvent( event: LnZapRequestEvent, signer: NostrSignerSync, - ): LnZapPrivateEvent? { - if (signer.keyPair.privKey == null) return null + ): LnZapPrivateEvent { + if (signer.keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() val recipientPK = event.zappedAuthor().firstOrNull() val recipientPost = event.zappedPost().firstOrNull() val privateEvent = if (recipientPK == signer.pubKey) { // if the receiver is logged in, these are the params. - val privateKeyToUse = signer.keyPair.privKey - val pubkeyToUse = event.pubKey - - event.getPrivateZapEvent(privateKeyToUse, pubkeyToUse) + decryptAnonTag(event.getAnonTag(), signer.keyPair.privKey, event.pubKey) } else { // if the sender is logged in, these are the params val altPubkeyToUse = recipientPK - val altPrivateKeyToUse = + val myPrivateKeyForThisEvent = if (recipientPost != null) { PrivateZapEncryption.createEncryptionPrivateKey( signer.keyPair.privKey.toHexKey(), @@ -100,35 +101,44 @@ class PrivateZapRequestBuilder { event.createdAt, ) } else { - null + throw IllegalArgumentException("Couldn't find a secret to use. The private zap is neither a post nor an author zap") } try { - if (altPrivateKeyToUse != null && altPubkeyToUse != null) { - val altPubKeyFromPrivate = Nip01.pubKeyCreate(altPrivateKeyToUse).toHexKey() + if (altPubkeyToUse != null) { + val altPubKeyFromPrivate = Nip01.pubKeyCreate(myPrivateKeyForThisEvent).toHexKey() if (altPubKeyFromPrivate == event.pubKey) { - val result = event.getPrivateZapEvent(altPrivateKeyToUse, altPubkeyToUse) - - if (result == null) { - Log.w( - "Private ZAP Decrypt", - "Fail to decrypt Zap from ${event.id}", - ) - } - result + // the sender is logged in. + decryptAnonTag(event.getAnonTag(), myPrivateKeyForThisEvent, altPubkeyToUse) } else { - null + throw IllegalArgumentException("This private zap cannot be decrypted by this key.") } } else { - null + throw IllegalArgumentException("Recipient pubkey not found.") } } catch (e: Exception) { - Log.e("Account", "Failed to create pubkey for ZapRequest ${event.id}", e) - null + throw IllegalArgumentException("Failed to create pubkey for ZapRequest ${event.id}. ${e.message}") } } return privateEvent } + + fun decryptAnonTag( + encNote: String, + privateKey: ByteArray, + pubKey: HexKey, + ): LnZapPrivateEvent = + try { + val note = PrivateZapEncryption.decryptPrivateZapMessage(encNote, privateKey, pubKey.hexToByteArray()) + val decryptedEvent = fromJson(note) + if (decryptedEvent.kind == 9733) { + decryptedEvent as LnZapPrivateEvent + } else { + throw IllegalStateException("The decrypted event is not a private zap.") + } + } catch (e: Exception) { + throw IllegalStateException("Could not decrypt private zap. ${e.message}") + } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/RumorAssembler.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/RumorAssembler.kt new file mode 100644 index 0000000000..65e7a61fc7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/rumors/RumorAssembler.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip59Giftwrap.rumors + +import com.vitorpamplona.quartz.EventFactory +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate + +class RumorAssembler { + companion object { + fun assembleRumor( + pubKey: HexKey, + ev: EventTemplate, + ) = assembleRumor( + pubKey, + ev.createdAt, + ev.kind, + ev.tags, + ev.content, + ) + + fun assembleRumor( + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + ) = EventFactory.create( + id = EventHasher.hashId(pubKey, createdAt, kind, tags, content), + pubKey = pubKey, + createdAt = createdAt, + kind = kind, + tags = tags, + content = content, + sig = "", + ) as T + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt index e580f23628..c65005c192 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt @@ -66,65 +66,59 @@ class SealedRumorEvent( override fun isContentEncoded() = true - fun unseal( - signer: NostrSigner, - onReady: (Event) -> Unit, - ) { - try { - plainContent(signer) { - try { - val rumor = Rumor.fromJson(it) - val event = rumor.mergeWith(this) - if (event is WrappedEvent) { - event.host = host ?: HostStub(this.id, this.pubKey, this.kind) - } - innerEventId = event.id + suspend fun unsealThrowing(signer: NostrSigner): Event { + val rumor = Rumor.fromJson(plainContent(signer)) - onReady(event) - } catch (e: Exception) { - Log.w("RumorEvent", "Fail to decrypt or parse Rumor", e) - } - } + val event = rumor.mergeWith(this) + if (event is WrappedEvent) { + event.host = host ?: HostStub(this.id, this.pubKey, this.kind) + } + innerEventId = event.id + + return event + } + + suspend fun unsealOrNull(signer: NostrSigner): Event? { + return try { + return unsealThrowing(signer) } catch (e: Exception) { Log.w("RumorEvent", "Fail to decrypt or parse Rumor", e) + null } } - private fun plainContent( - signer: NostrSigner, - onReady: (String) -> Unit, - ) { - if (content.isEmpty()) return + private suspend fun plainContent(signer: NostrSigner): String { + if (content.isEmpty()) return "" - signer.nip44Decrypt(content, pubKey, onReady) + return signer.nip44Decrypt(content, pubKey) } companion object { const val KIND = 13 - fun create( + suspend fun create( event: Event, encryptTo: HexKey, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (SealedRumorEvent) -> Unit, - ) { + ): SealedRumorEvent { val rumor = Rumor.create(event) - create(rumor, encryptTo, signer, createdAt, onReady) + return create(rumor, encryptTo, signer, createdAt) } - fun create( + suspend fun create( rumor: Rumor, encryptTo: HexKey, signer: NostrSigner, createdAt: Long = TimeUtils.randomWithTwoDays(), - onReady: (SealedRumorEvent) -> Unit, - ) { + ): SealedRumorEvent { val msg = Rumor.toJson(rumor) - - signer.nip44Encrypt(msg, encryptTo) { content -> - signer.sign(createdAt, KIND, emptyArray(), content, onReady) - } + return signer.sign( + createdAt = createdAt, + kind = KIND, + tags = emptyArray(), + content = signer.nip44Encrypt(msg, encryptTo), + ) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt index 73cc4ec438..9b8fd93130 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt @@ -68,40 +68,31 @@ class GiftWrapEvent( override fun isContentEncoded() = true - fun unwrapThrowing( - signer: NostrSigner, - onReady: (Event) -> Unit, - ) { - plainContent(signer) { giftStr -> - val gift = fromJson(giftStr) + suspend fun unwrapThrowing(signer: NostrSigner): Event { + val giftStr = plainContent(signer) + val gift = fromJson(giftStr) - if (gift is WrappedEvent) { - gift.host = HostStub(this.id, this.pubKey, this.kind) - } - innerEventId = gift.id - - onReady(gift) + if (gift is WrappedEvent) { + gift.host = HostStub(this.id, this.pubKey, this.kind) } + innerEventId = gift.id + + return gift } - fun unwrap( - signer: NostrSigner, - onReady: (Event) -> Unit, - ) { + suspend fun unwrapOrNull(signer: NostrSigner): Event? { try { - unwrapThrowing(signer, onReady) - } catch (e: Exception) { + return unwrapThrowing(signer) + } catch (_: Exception) { Log.w("GiftWrapEvent", "Couldn't Decrypt the content " + this.toNostrUri()) + return null } } - private fun plainContent( - signer: NostrSigner, - onReady: (String) -> Unit, - ) { - if (content.isEmpty()) return + private suspend fun plainContent(signer: NostrSigner): String { + if (content.isEmpty()) return "" - signer.nip44Decrypt(content, pubKey, onReady) + return signer.nip44Decrypt(content, pubKey) } fun recipientPubKey() = tags.firstTagValue("p") @@ -110,19 +101,18 @@ class GiftWrapEvent( const val KIND = 1059 const val ALT = "Encrypted event" - fun create( + suspend fun create( event: Event, recipientPubKey: HexKey, createdAt: Long = TimeUtils.randomWithTwoDays(), - onReady: (GiftWrapEvent) -> Unit, - ) { + ): GiftWrapEvent { val signer = NostrSignerInternal(KeyPair()) // GiftWrap is always a random key - val serializedContent = event.toJson() - val tags = arrayOf(arrayOf("p", recipientPubKey)) - - signer.nip44Encrypt(serializedContent, recipientPubKey) { content -> - signer.sign(createdAt, KIND, tags, content, onReady) - } + return signer.sign( + createdAt = createdAt, + kind = KIND, + tags = arrayOf(arrayOf("p", recipientPubKey)), + content = signer.nip44Encrypt(event.toJson(), recipientPubKey), + ) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt index 8b1078e666..315f406b3d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip65RelayList/AdvertisedRelayListEvent.kt @@ -59,47 +59,42 @@ class AdvertisedRelayListEvent( fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, FIXED_D_TAG) - fun replaceRelayListWith( + suspend fun replaceRelayListWith( earlierVersion: AdvertisedRelayListEvent, newRelays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (AdvertisedRelayListEvent) -> Unit, - ) { + ): AdvertisedRelayListEvent { val tags = earlierVersion.tags.filter(AdvertisedRelayInfo::notMatch) val relayTags = newRelays.map { it.toTagArray() } - signer.sign(createdAt, KIND, (tags + relayTags).toTypedArray(), earlierVersion.content, onReady) + return signer.sign(createdAt, KIND, (tags + relayTags).toTypedArray(), earlierVersion.content) } - fun createFromScratch( + suspend fun createFromScratch( relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (AdvertisedRelayListEvent) -> Unit, - ) = create(relays, signer, createdAt, onReady) + ) = create(relays, signer, createdAt) - fun create( + suspend fun create( list: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (AdvertisedRelayListEvent) -> Unit, - ) { + ): AdvertisedRelayListEvent { val tags = list.map { it.toTagArray() }.toTypedArray() - val msg = "" - signer.sign(createdAt, KIND, tags, msg, onReady) + return signer.sign(createdAt, KIND, tags, "") } fun create( list: List, signer: NostrSignerSync, createdAt: Long = TimeUtils.now(), - ): AdvertisedRelayListEvent? { + ): AdvertisedRelayListEvent { val tags = list.map { it.toTagArray() }.toTypedArray() - val msg = "" - return signer.sign(createdAt, KIND, tags, msg) + return signer.sign(createdAt, KIND, tags, "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt index 747bc9f4d8..aaaaab456e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/CommunityListEvent.kt @@ -22,19 +22,23 @@ package com.vitorpamplona.quartz.nip72ModCommunities.follow import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.fastAny +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner -import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip51Lists.GeneralListEvent -import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayBuilder -import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.nip51Lists.removeAny +import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.tryAndWait -import kotlin.coroutines.resume +import kotlin.collections.plus @Immutable class CommunityListEvent( @@ -44,40 +48,15 @@ class CommunityListEvent( tags: Array>, content: String, sig: HexKey, -) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient var publicAndPrivateAddressCache: Set? = null +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig), + AddressHintProvider { + override fun addressHints() = tags.mapNotNull(CommunityTag::parseAsHint) - fun publicCommunities() = tags.mapNotNull(ATag::parseAsHint) + override fun linkedAddressIds() = tags.mapNotNull(CommunityTag::parseAddressId) - fun publicCommunityIds() = tags.mapNotNull(ATag::parseAddressId) + fun publicCommunities() = tags.communities() - fun publicAndCachedPrivateCommunityIds() = publicCommunityIds() + (publicAndPrivateAddressCache?.map { it.addressId } ?: emptyList()) - - fun publicAndPrivateCommunities( - signer: NostrSigner, - onReady: (Set) -> Unit, - ) { - publicAndPrivateAddressCache?.let { eventList -> - onReady(eventList) - return - } - - mergeTagList(signer) { - val set = it.mapNotNull(ATag::parseAsHint).toSet() - publicAndPrivateAddressCache = set - onReady(set) - } - } - - suspend fun publicAndPrivateCommunities(signer: NostrSigner): Set? { - publicAndPrivateAddressCache?.let { return it } - - return tryAndWait { continuation -> - publicAndPrivateCommunities(signer) { privateTagList -> - continuation.resume(privateTagList) - } - } - } + fun publicCommunityIds() = tags.communityIds() companion object { const val KIND = 10004 @@ -86,186 +65,166 @@ class CommunityListEvent( fun createAddress(pubKey: HexKey) = Address(KIND, pubKey, FIXED_D_TAG) - private fun createCommunityBase( - tags: Array>, + suspend fun create( + communities: List, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.create( - tags, - isPrivate, - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) + ): CommunityListEvent = + if (isPrivate) { + create( + publicCommunities = emptyList(), + privateCommunities = communities, + signer = signer, + createdAt = createdAt, + ) + } else { + create( + publicCommunities = communities, + privateCommunities = emptyList(), + signer = signer, + createdAt = createdAt, + ) } + + suspend fun create( + community: CommunityTag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): CommunityListEvent = + if (isPrivate) { + create( + publicCommunities = emptyList(), + privateCommunities = listOf(community), + signer = signer, + createdAt = createdAt, + ) + } else { + create( + publicCommunities = listOf(community), + privateCommunities = emptyList(), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun add( + earlierVersion: CommunityListEvent, + communities: List, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): CommunityListEvent = + if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + resign( + tags = earlierVersion.tags, + privateTags = privateTags.removeAny(communities.map { it.toTagIdOnly() }) + communities.map { it.toTagArray() }, + signer = signer, + createdAt = createdAt, + ) + } else { + resign( + content = earlierVersion.content, + tags = earlierVersion.tags.removeAny(communities.map { it.toTagIdOnly() }) + communities.map { it.toTagArray() }, + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun add( + earlierVersion: CommunityListEvent, + community: CommunityTag, + isPrivate: Boolean, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): CommunityListEvent = + if (isPrivate) { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + resign( + tags = earlierVersion.tags, + privateTags = privateTags.plus(community.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } else { + resign( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(community.toTagArray()), + signer = signer, + createdAt = createdAt, + ) + } + + suspend fun remove( + earlierVersion: CommunityListEvent, + community: CommunityTag, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): CommunityListEvent { + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + return resign( + privateTags = privateTags.remove(community.toTagIdOnly()), + tags = earlierVersion.tags.remove(community.toTagIdOnly()), + signer = signer, + createdAt = createdAt, + ) } - fun createCommunity( - community: EventHintBundle, - isPrivate: Boolean, + suspend fun resign( + tags: TagArray, + privateTags: TagArray, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) = createCommunityBase( - tags = arrayOf(ATag.assemble(community.event.address(), community.relay)), - isPrivate = isPrivate, + ) = resign( + content = PrivateTagsInContent.encryptNip04(privateTags, signer), + tags = tags, signer = signer, createdAt = createdAt, - onReady = onReady, ) - fun createCommunity( - community: ATag, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) = createCommunityBase( - tags = arrayOf(community.toATagArray()), - isPrivate = isPrivate, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - - fun createCommunities( - communities: List>, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) = createCommunityBase( - tags = communities.map { ATag.assemble(it.event.address().toValue(), it.relay) }.toTypedArray(), - isPrivate = isPrivate, - signer = signer, - createdAt = createdAt, - onReady = onReady, - ) - - fun removeCommunity( - earlierVersion: CommunityListEvent, - community: String, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.removeAll( - earlierVersion, - ATag.assemble(community, null), - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - private fun addCommunityBase( - earlierVersion: CommunityListEvent, - newTags: Array>, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) { - PrivateTagArrayBuilder.addAll( - earlierVersion, - newTags, - isPrivate, - signer, - ) { encryptedContent, newTags -> - create(encryptedContent, newTags, signer, createdAt, onReady) - } - } - - fun addCommunity( - earlierVersion: CommunityListEvent, - community: ATag, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) = addCommunityBase( - earlierVersion, - arrayOf(community.toATagArray()), - isPrivate, - signer, - createdAt, - onReady, - ) - - fun addCommunity( - earlierVersion: CommunityListEvent, - community: EventHintBundle, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) = addCommunityBase( - earlierVersion, - arrayOf(community.toATag().toATagArray()), - isPrivate, - signer, - createdAt, - onReady, - ) - - fun addCommunity( - earlierVersion: CommunityListEvent, - community: AddressHint, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) = addCommunityBase( - earlierVersion, - arrayOf(ATag.assemble(community.addressId, community.relay)), - isPrivate, - signer, - createdAt, - onReady, - ) - - fun addCommunities( - earlierVersion: CommunityListEvent, - communities: List>, - isPrivate: Boolean, - signer: NostrSigner, - createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) = addCommunityBase( - earlierVersion, - communities.map { it.toATag().toATagArray() }.toTypedArray(), - isPrivate, - signer, - createdAt, - onReady, - ) - - private fun create( + suspend fun resign( content: String, - tags: Array>, + tags: TagArray, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (CommunityListEvent) -> Unit, - ) { + ): CommunityListEvent { val newTags = - if (tags.any { it.size > 1 && it[0] == "alt" }) { + if (tags.fastAny(AltTag::match)) { tags } else { tags + AltTag.assemble(ALT) } - signer.sign(createdAt, KIND, newTags, content, onReady) + return signer.sign(createdAt, KIND, newTags, content) } - fun create( - list: List, - signer: NostrSignerSync, + suspend fun create( + publicCommunities: List = emptyList(), + privateCommunities: List = emptyList(), + signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): CommunityListEvent? { - val tags = list.map { ATag.assemble(it.addressId, it.relay) }.toTypedArray() - return signer.sign(createdAt, KIND, tags, "") + ): CommunityListEvent { + val template = build(publicCommunities, privateCommunities, signer, createdAt) + return signer.sign(template) + } + + suspend fun build( + publicCommunities: List = emptyList(), + privateCommunities: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip04(privateCommunities.map { it.toTagArray() }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + communities(publicCommunities) + + initializer() } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayBuilderExt.kt index 098657d269..af274d9efc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayBuilderExt.kt @@ -24,6 +24,12 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip51Lists.tags.NameTag +import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag + +fun TagArrayBuilder.name(name: String) = addUnique(NameTag.assemble(name)) + +fun TagArrayBuilder.communities(communities: List) = addAll(communities.map { it.toTagArray() }) fun TagArrayBuilder.followCommunity( address: Address, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayExt.kt new file mode 100644 index 0000000000..ca2b50b67b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/TagArrayExt.kt @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.follow + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip72ModCommunities.follow.tags.CommunityTag + +fun TagArray.communities() = mapNotNull(CommunityTag::parse) + +fun TagArray.communityIds() = mapNotNull(CommunityTag::parseAddressId) + +fun TagArray.communityIdSet() = mapNotNullTo(mutableSetOf(), CommunityTag::parseValidAddress) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt new file mode 100644 index 0000000000..7b6bcc6d35 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip72ModCommunities.follow.tags + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.ensure +import com.vitorpamplona.quartz.utils.pointerSizeInBytes + +class CommunityTag( + val address: Address, + val relayHint: NormalizedRelayUrl? = null, +) { + fun countMemory(): Long = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) + + fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) + + fun toTagArray() = assemble(address, relayHint) + + fun toTagIdOnly() = assemble(address, null) + + companion object { + const val TAG_NAME = "a" + + @JvmStatic + fun isTagged(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun isTagged( + tag: Array, + addressId: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == addressId + + @JvmStatic + fun isTagged( + tag: Array, + address: CommunityTag, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag() + + @JvmStatic + fun isIn( + tag: Array, + addressIds: Set, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds + + @JvmStatic + fun isTaggedWithKind( + tag: Array, + kind: String, + ) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], kind) + + @JvmStatic + fun parse( + aTagId: String, + relay: String?, + ) = Address.parse(aTagId)?.let { + CommunityTag(it, relay?.let { RelayUrlNormalizer.normalizeOrNull(it) }) + } + + @JvmStatic + fun parse(tag: Array): CommunityTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return parse(tag[1], tag.getOrNull(2)) + } + + @JvmStatic + fun parseValidAddress(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return Address.parse(tag[1])?.toValue() + } + + @JvmStatic + fun parseAddress(tag: Array): Address? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return Address.parse(tag[1]) + } + + @JvmStatic + fun parseAddressId(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + @JvmStatic + fun parseAsHint(tag: Array): AddressHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + ensure(tag[1].contains(':')) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return AddressHint(tag[1], relayHint) + } + + @JvmStatic + fun assemble( + aTagId: HexKey, + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url) + + @JvmStatic + fun assemble( + address: Address, + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, address.toValue(), relay?.url) + + @JvmStatic + fun assemble( + kind: Int, + pubKey: String, + dTag: String, + relay: NormalizedRelayUrl?, + ) = assemble(Address.assemble(kind, pubKey, dTag), relay) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt index c3725b4651..6dcd9a31eb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip78AppData/AppSpecificDataEvent.kt @@ -52,14 +52,13 @@ class AppSpecificDataEvent( dTag: String, ): ATag = ATag(KIND, pubkey, dTag, null) - fun create( + suspend fun create( dTag: String, description: String, otherTags: Array>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (AppSpecificDataEvent) -> Unit, - ) { + ): AppSpecificDataEvent { val withD = if (otherTags.any { it.size > 1 && it[0] == "d" && it[1] == dTag }) { otherTags @@ -74,7 +73,7 @@ class AppSpecificDataEvent( withD } - signer.sign(createdAt, KIND, newTags, description, onReady) + return signer.sign(createdAt, KIND, newTags, description) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt index 3daf227059..5e8254e91d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt @@ -130,13 +130,10 @@ class HighlightEvent( const val KIND = 9802 const val ALT = "Highlight/quote event" - fun create( + suspend fun create( msg: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (HighlightEvent) -> Unit, - ) { - signer.sign(createdAt, KIND, arrayOf(AltTag.assemble(ALT)), msg, onReady) - } + ): HighlightEvent = signer.sign(createdAt, KIND, arrayOf(AltTag.assemble(ALT)), msg) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt index 7c22362036..f2d77522ea 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip89AppHandlers.PlatformType import com.vitorpamplona.quartz.nip89AppHandlers.definition.tags.PlatformLinkTag import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.CancellationException import java.util.UUID @Immutable @@ -66,8 +67,8 @@ class AppDefinitionEvent( newMetadata } } catch (e: Exception) { - e.printStackTrace() - Log.w("AppDefinitionEvent", "Content Parse Error: ${toNostrUri()} ${e.localizedMessage}") + if (e is CancellationException) throw e + Log.w("AppDefinitionEvent", "Content Parse Error: ${toNostrUri()} ${e.localizedMessage}", e) null } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt index c31dd4a47e..addd3c39c9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryRequestEvent.kt @@ -43,14 +43,13 @@ class NIP90ContentDiscoveryRequestEvent( const val KIND = 5300 const val ALT = "NIP90 Content Discovery request" - fun create( + suspend fun create( dvmPublicKey: HexKey, forUser: HexKey, relays: Set, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (NIP90ContentDiscoveryRequestEvent) -> Unit, - ) { + ): NIP90ContentDiscoveryRequestEvent { val content = "" val tags = mutableListOf>() tags.add(arrayOf("p", dvmPublicKey)) @@ -58,7 +57,7 @@ class NIP90ContentDiscoveryRequestEvent( tags.add(arrayOf("relays") + relays.map { it.url }.toTypedArray()) tags.add(arrayOf("param", "max_results", "200")) tags.add(arrayOf("param", "user", forUser)) - signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + return signer.sign(createdAt, KIND, tags.toTypedArray(), content) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt index da692ad583..d7ca0d2ac7 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes @@ -77,16 +76,15 @@ class NIP90ContentDiscoveryResponseEvent( const val KIND = 6300 const val ALT = "NIP90 Content Discovery reply" - fun create( + suspend fun create( signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (AppRecommendationEvent) -> Unit, - ) { + ): NIP90ContentDiscoveryResponseEvent { val tags = arrayOf( AltTag.assemble(ALT), ) - signer.sign(createdAt, KIND, tags, "", onReady) + return signer.sign(createdAt, KIND, tags, "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90StatusEvent.kt index 950aecf242..a36411df7f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90StatusEvent.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -78,16 +77,15 @@ class NIP90StatusEvent( const val KIND = 7000 const val ALT = "NIP90 Status update" - fun create( + suspend fun create( signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (AppRecommendationEvent) -> Unit, - ) { + ): NIP90StatusEvent { val tags = arrayOf( AltTag.assemble(ALT), ) - signer.sign(createdAt, KIND, tags, "", onReady) + return signer.sign(createdAt, KIND, tags, "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryRequestEvent.kt index 414c929743..6d4cf85e5e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryRequestEvent.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -41,16 +40,15 @@ class NIP90UserDiscoveryRequestEvent( const val KIND = 5301 const val ALT = "NIP90 Content Discovery request" - fun create( + suspend fun create( signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (AppRecommendationEvent) -> Unit, - ) { + ): NIP90UserDiscoveryRequestEvent { val tags = arrayOf( AltTag.assemble(ALT), ) - signer.sign(createdAt, KIND, tags, "", onReady) + return signer.sign(createdAt, KIND, tags, "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryResponseEvent.kt index c4d9978345..a3e619c751 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip90Dvms/NIP90UserDiscoveryResponseEvent.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag -import com.vitorpamplona.quartz.nip89AppHandlers.recommendation.AppRecommendationEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -41,16 +40,15 @@ class NIP90UserDiscoveryResponseEvent( const val KIND = 6301 const val ALT = "NIP90 Content Discovery reply" - fun create( + suspend fun create( signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (AppRecommendationEvent) -> Unit, - ) { + ): NIP90UserDiscoveryResponseEvent { val tags = arrayOf( AltTag.assemble(ALT), ) - signer.sign(createdAt, KIND, tags, "", onReady) + return signer.sign(createdAt, KIND, tags, "") } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt index d2db684831..ca00e94991 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomAuthorizationEvent.kt @@ -38,47 +38,42 @@ class BlossomAuthorizationEvent( companion object { const val KIND = 24242 - fun createGetAuth( + suspend fun createGetAuth( hash: HexKey, alt: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (BlossomAuthorizationEvent) -> Unit, - ) = createAuth("get", hash, null, alt, signer, createdAt, onReady) + ) = createAuth("get", hash, null, alt, signer, createdAt) - fun createListAuth( + suspend fun createListAuth( signer: NostrSigner, alt: String, createdAt: Long = TimeUtils.now(), - onReady: (BlossomAuthorizationEvent) -> Unit, - ) = createAuth("list", null, null, alt, signer, createdAt, onReady) + ) = createAuth("list", null, null, alt, signer, createdAt) - fun createDeleteAuth( + suspend fun createDeleteAuth( hash: HexKey, alt: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (BlossomAuthorizationEvent) -> Unit, - ) = createAuth("delete", hash, null, alt, signer, createdAt, onReady) + ) = createAuth("delete", hash, null, alt, signer, createdAt) - fun createUploadAuth( + suspend fun createUploadAuth( hash: HexKey, size: Long, alt: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (BlossomAuthorizationEvent) -> Unit, - ) = createAuth("upload", hash, size, alt, signer, createdAt, onReady) + ) = createAuth("upload", hash, size, alt, signer, createdAt) - private fun createAuth( + private suspend fun createAuth( type: String, hash: HexKey?, fileSize: Long?, alt: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (BlossomAuthorizationEvent) -> Unit, - ) { + ): BlossomAuthorizationEvent { val tags = listOfNotNull( arrayOf("t", type), @@ -87,7 +82,7 @@ class BlossomAuthorizationEvent( hash?.let { arrayOf("x", it) }, ) - signer.sign(createdAt, KIND, tags.toTypedArray(), alt, onReady) + return signer.sign(createdAt, KIND, tags.toTypedArray(), alt) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt index 817ed20d29..9e2c125702 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt @@ -64,13 +64,12 @@ class BlossomServersEvent( }.plusElement(AltTag.assemble(ALT)) .toTypedArray() - fun updateRelayList( + suspend fun updateRelayList( earlierVersion: BlossomServersEvent, relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (BlossomServersEvent) -> Unit, - ) { + ): BlossomServersEvent { val tags = earlierVersion.tags .filter { it[0] != "server" } @@ -80,25 +79,19 @@ class BlossomServersEvent( }, ).toTypedArray() - signer.sign(createdAt, KIND, tags, earlierVersion.content, onReady) + return signer.sign(createdAt, KIND, tags, earlierVersion.content) } - fun createFromScratch( + suspend fun createFromScratch( relays: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (BlossomServersEvent) -> Unit, - ) { - create(relays, signer, createdAt, onReady) - } + ) = create(relays, signer, createdAt) - fun create( + suspend fun create( servers: List, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - onReady: (BlossomServersEvent) -> Unit, - ) { - signer.sign(createdAt, KIND, createTagArray(servers), "", onReady) - } + ) = signer.sign(createdAt, KIND, createTagArray(servers), "") } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt index 271175ddc4..db36dd7cef 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt @@ -40,6 +40,8 @@ fun Array.startsWith(startsWith: Array): Boolean { return true } +fun Array.startsWithAny(startsWithList: List>): Boolean = startsWithList.any { startsWith(it) } + public inline fun Array.lastNotNullOfOrNull(transform: (T) -> R?): R? { for (index in this.indices.reversed()) { val result = transform(this[index]) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LibSodiumInstance.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LibSodiumInstance.kt index b8700c6069..6c327f4238 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LibSodiumInstance.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LibSodiumInstance.kt @@ -82,7 +82,7 @@ object LibSodiumInstance { messageBytes: ByteArray, nonce: ByteArray, key: ByteArray, - ): ByteArray? { + ): ByteArray { val cipher = ByteArray(messageBytes.size) val k2 = ByteArray(32) @@ -100,6 +100,6 @@ object LibSodiumInstance { k2, ) - return if (resultCode == 0) cipher else null + return if (resultCode == 0) cipher else throw IllegalStateException("Could not decrypt message") } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt index 4e32f16a41..b47f4250b4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ParallelUtils.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.utils +import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.cancel @@ -28,7 +29,6 @@ import kotlinx.coroutines.joinAll import kotlinx.coroutines.selects.select import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull -import kotlin.coroutines.Continuation import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume @@ -66,7 +66,7 @@ suspend fun launchAndWaitAll( */ suspend inline fun tryAndWait( timeoutMillis: Long = 10000, - crossinline asyncFunc: (Continuation) -> Unit, + crossinline asyncFunc: (CancellableContinuation) -> Unit, ): T? = withTimeoutOrNull(timeoutMillis) { suspendCancellableCoroutine { continuation -> @@ -156,6 +156,36 @@ suspend fun mapNotNullAsync( } } +@OptIn(ExperimentalCoroutinesApi::class) +suspend fun mapNotNullAsyncSimple( + items: List, + runRequestFor: suspend (T) -> K?, +): List { + if (items.isEmpty()) { + return emptyList() + } + + return coroutineScope { + // Launch all tasks asynchronously and get their Deferred handles. + val jobs = + items.map { item -> + async { + runRequestFor(item) + } + } + + jobs.joinAll() + + jobs.mapNotNull { + if (it.isCompleted) { + it.getCompleted() + } else { + null + } + } + } +} + /** * Executes a mapping function asynchronously on each input in the list. * Returns true as soon as the first mapping returns true, cancelling all other ongoing operations. diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt index 13fc92809f..45d83bfc9e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/TimeUtils.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.utils object TimeUtils { + const val TEN_SECONDS = 10 const val ONE_MINUTE = 60 const val FIVE_MINUTES = 5 * ONE_MINUTE const val FIFTEEN_MINUTES = 15 * ONE_MINUTE @@ -33,6 +34,8 @@ object TimeUtils { fun now() = System.currentTimeMillis() / 1000 + fun tenSecondsFromNow() = now() + TEN_SECONDS + fun oneMinuteFromNow() = now() + ONE_MINUTE fun oneMinuteAgo() = now() - ONE_MINUTE From 9d9963d32458a17b8afccb459a6b8b562e66a001 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 18 Jul 2025 17:47:11 -0400 Subject: [PATCH 098/185] Fixes saving old version of event in to the cache. --- .../src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index fa0a0b2a1f..9fbaf9fbc5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1288,7 +1288,6 @@ class Account( cache.justConsumeMyOwnEvent(deletedDraftEvent) delete(deletedDraftEvent, note.relays.toSet()) - draftsDecryptionCache.delete(noteEvent) } } } From 85dd9dd5a4cd4ef02dd186db606dadda5c6929c8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 18 Jul 2025 17:47:56 -0400 Subject: [PATCH 099/185] Fixes screen clean up in Login --- .../ui/screen/loggedOff/login/LoginViewModel.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt index b113f15159..eab3f7b756 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/LoginViewModel.kt @@ -60,13 +60,27 @@ class LoginViewModel : ViewModel() { isFirstLogin: Boolean, newAccountKey: String?, ) { + clear() this.isFirstLogin = isFirstLogin - acceptedTerms = isFirstLogin + acceptedTerms = !isFirstLogin if (newAccountKey != null) { key = TextFieldValue(newAccountKey) } } + fun clear() { + key = TextFieldValue("") + password = TextFieldValue("") + + errorManager.clearErrors() + acceptedTerms = false + processingLogin = false + isTemporary = false + offerTemporaryLogin = false + torSettings = TorSettings() + isFirstLogin = false + } + fun updateKey( value: TextFieldValue, throughQR: Boolean, From 19327b682c1d4d40c1b77d8609c82f9e018a4681 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 18 Jul 2025 17:51:43 -0400 Subject: [PATCH 100/185] Improves external signer logs --- .../ui/screen/loggedOff/login/ExternalSignerButton.kt | 2 +- .../api/foreground/IntentRequestManager.kt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt index 0cc8ba4273..d3b2d274a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt @@ -74,7 +74,7 @@ fun ExternalSignerButton(loginViewModel: LoginViewModel) { launcher.launch(ExternalSignerLogin.createIntent(DefaultSignerPermissions)) } catch (e: Exception) { if (e is CancellationException) throw e - Log.e("Signer", "Error opening Signer app", e) + Log.e("ExternalSigner", "Error opening Signer app", e) loginViewModel.errorManager.error(R.string.error_opening_external_signer) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt index 8b06b2387e..d10768450b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground import android.content.ActivityNotFoundException import android.content.Intent +import android.util.Log +import android.util.Log.e import androidx.collection.LruCache import com.vitorpamplona.quartz.nip55AndroidSigner.api.IResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult @@ -119,10 +121,12 @@ class IntentRequestManager( try { launcher.invoke(requestIntent) - } catch (_: ActivityNotFoundException) { + } catch (e: ActivityNotFoundException) { + Log.e("ExternalSigner", "Error launching intent", e) awaitingRequests.remove(callId) continuation.resume(activityNotFoundIntent) } catch (e: Exception) { + Log.e("ExternalSigner", "Error launching intent", e) awaitingRequests.remove(callId) continuation.resume(null) if (e is CancellationException) throw e From ee67f2ded99d1e2384c7ac9f6cba214cc151fa4a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 18 Jul 2025 18:33:50 -0400 Subject: [PATCH 101/185] Adds a subscription to download notifications from random relays in case users are not outbox ready yet. --- .../account/AccountFilterAssembler.kt | 6 +- ...otificationsEoseFromInboxRelaysManager.kt} | 7 +- ...otificationsEoseFromRandomRelaysManager.kt | 80 +++++++++++++++++++ .../FilterNotificationsToPubkey.kt | 43 +++++++++- 4 files changed, 132 insertions(+), 4 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/{AccountNotificationsEoseManager.kt => AccountNotificationsEoseFromInboxRelaysManager.kt} (92%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt index 68fea5dac8..6795ee287f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt @@ -23,7 +23,8 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadata.AccountMetadataEoseManager -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromInboxRelaysManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromRandomRelaysManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps.AccountGiftWrapsEoseManager import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient @@ -44,7 +45,8 @@ class AccountFilterAssembler( listOf( AccountMetadataEoseManager(client, ::allKeys), AccountGiftWrapsEoseManager(client, ::allKeys), - AccountNotificationsEoseManager(client, ::allKeys), + AccountNotificationsEoseFromInboxRelaysManager(client, ::allKeys), + AccountNotificationsEoseFromRandomRelaysManager(client, ::allKeys), ) override fun invalidateKeys() = invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt index 331080ef65..d4f2ff05ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt @@ -33,12 +33,17 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -class AccountNotificationsEoseManager( +class AccountNotificationsEoseFromInboxRelaysManager( client: NostrClient, allKeys: () -> Set, ) : PerUserEoseManager(client, allKeys) { override fun user(query: AccountQueryState) = query.account.userProfile() + /** + * Downloads most notifications from the user's own inbox relays. + * But also connects to all the follows relays to check for new notifications that are not in the user's + * own inbox. + */ override fun updateFilter( key: AccountQueryState, since: SincePerRelayMap?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt new file mode 100644 index 0000000000..cada0012fa --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications + +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +class AccountNotificationsEoseFromRandomRelaysManager( + client: NostrClient, + allKeys: () -> Set, +) : PerUserEoseManager(client, allKeys) { + override fun user(query: AccountQueryState) = query.account.userProfile() + + /** + * Downloads most notifications from the user's own inbox relays. + * But also connects to all the follows relays to check for new notifications that are not in the user's + * own inbox. + */ + override fun updateFilter( + key: AccountQueryState, + since: SincePerRelayMap?, + ): List? = + (key.account.followsPerRelay.value.keys - key.account.notificationRelays.flow.value).flatMap { + filterJustTheLatestNotificationsToPubkeyFromRandomRelays(it, user(key).pubkeyHex, since?.get(it)?.time) + } + + val userJobMap = mutableMapOf>() + + @OptIn(FlowPreview::class) + override fun newSub(key: AccountQueryState): Subscription { + val user = user(key) + userJobMap[user]?.forEach { it.cancel() } + userJobMap[user] = + listOf( + key.account.scope.launch(Dispatchers.Default) { + key.account.followsPerRelay.collectLatest { + invalidateFilters() + } + }, + ) + + return super.newSub(key) + } + + override fun endSub( + key: User, + subId: String, + ) { + super.endSub(key, subId) + userJobMap[key]?.forEach { it.cancel() } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt index 8a7ba5e3ee..16614cbb68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt @@ -100,7 +100,7 @@ fun filterNotificationsToPubkey( Filter( kinds = NotificationsPerKeyKinds2, tags = mapOf("p" to listOf(pubkey)), - limit = 400, + limit = 50, since = since, ), ), @@ -116,3 +116,44 @@ fun filterNotificationsToPubkey( ), ) } + +fun filterJustTheLatestNotificationsToPubkeyFromRandomRelays( + relay: NormalizedRelayUrl, + pubkey: HexKey?, + since: Long?, +): List { + if (pubkey == null || pubkey.isEmpty()) return emptyList() + + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = NotificationsPerKeyKinds, + tags = mapOf("p" to listOf(pubkey)), + limit = 10, + since = since, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = NotificationsPerKeyKinds2, + tags = mapOf("p" to listOf(pubkey)), + limit = 10, + since = since, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = NotificationsPerKeyKinds3, + tags = mapOf("p" to listOf(pubkey)), + limit = 10, + since = since, + ), + ), + ) +} From 13ce0e731131d85c1baf252d64280d11df22a417 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 18 Jul 2025 18:34:54 -0400 Subject: [PATCH 102/185] Breaks down the amount of reactions for each post to download from the amount of reactions to communities and chats themselves (which is much larger) --- .../FilterRepliesAndReactionsToAddresses.kt | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt index f062f2982a..b010546033 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/FilterRepliesAndReactionsToAddresses.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.utils.mapOfSet -val RepliesAndReactiionsToAddressesKinds1 = +val RepliesAndReactionsToAddressesKinds1 = listOf( TextNoteEvent.KIND, ReactionEvent.KIND, @@ -45,6 +45,10 @@ val RepliesAndReactiionsToAddressesKinds1 = ReportEvent.KIND, LnZapEvent.KIND, PollNoteEvent.KIND, + ) + +val PostsAndChatMessagesToAddresses = + listOf( CommunityPostApprovalEvent.KIND, LiveActivitiesChatMessageEvent.KIND, ) @@ -80,13 +84,24 @@ fun filterRepliesAndReactionsToAddresses( relay = relay, filter = Filter( - kinds = RepliesAndReactiionsToAddressesKinds1, + kinds = RepliesAndReactionsToAddressesKinds1, tags = mapOf("a" to sortedList), since = since, // Max amount of "replies" to download on a specific event. limit = 1000, ), ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = PostsAndChatMessagesToAddresses, + tags = mapOf("a" to sortedList), + since = since, + // Max amount of "replies" to download on a specific event. + limit = 100, + ), + ), RelayBasedFilter( relay = relay, filter = From 8a3bb6e1ae2a5f9e6d45a05cd2fa18d998a107d7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 18 Jul 2025 18:35:52 -0400 Subject: [PATCH 103/185] Significantly reduces the limit in our filters to reduce the amount of duplicated events coming from multiple relays. --- .../subassemblies/FilterLongFormByAllCommunities.kt | 4 ++-- .../subassemblies/FilterLongFormByAuthors.kt | 2 +- .../subassemblies/FilterLongFormByCommunity.kt | 4 ++-- .../subassemblies/FilterLongFormByGeohash.kt | 2 +- .../subassemblies/FilterLongFormByHashtag.kt | 2 +- .../nip23LongForm/subassemblies/FilterLongFormGlobal.kt | 2 +- .../nip28Chats/subassemblies/FilterPublicChatsGlobal.kt | 2 +- .../subassemblies/FilterFollowSetsGlobal.kt | 2 +- .../subassemblies/FilterLiveActivitiesGlobal.kt | 2 +- .../subassemblies/FilterCommunitiesByGeohash.kt | 2 +- .../subassemblies/FilterCommunitiesByHashtag.kt | 2 +- .../subassemblies/FilterCommunitiesGlobal.kt | 2 +- .../nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt | 2 +- .../subassemblies/FilterClassifiedsGlobal.kt | 2 +- .../nip72Communities/FilterHomePostsFromCommunities.kt | 4 ++-- .../nip01Core/FilterPictureAndVideoByGeohash.kt | 4 ++-- .../nip01Core/FilterPictureAndVideoByHashtag.kt | 4 ++-- .../nip01Core/FilterPictureAndVideoGlobal.kt | 4 ++-- .../nip65Follows/FilterPictureAndVideoByAuthors.kt | 4 ++-- .../FilterPictureAndVideoByAllCommunities.kt | 8 ++++---- 20 files changed, 30 insertions(+), 30 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt index b1ac3ea3fd..5a653f22aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt @@ -47,7 +47,7 @@ fun filterLongFormAllCommunities( "a" to communityList, "k" to listOf(LongTextNoteEvent.KIND.toString()), ), - limit = 300, + limit = 30, since = since, ), ), @@ -58,7 +58,7 @@ fun filterLongFormAllCommunities( Filter( tags = mapOf("k" to listOf("5300"), "a" to communityList), kinds = listOf(LongTextNoteEvent.KIND), - limit = 300, + limit = 30, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt index eb58737988..82cf18398b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt @@ -42,7 +42,7 @@ fun filterLongFormAuthors( Filter( authors = authorList, kinds = listOf(LongTextNoteEvent.KIND), - limit = 300, + limit = 30, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt index 55553bcce1..709c1d5deb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt @@ -48,7 +48,7 @@ fun filterLongFormByCommunity( "a" to listOf(community), "k" to listOf(LongTextNoteEvent.KIND.toString()), ), - limit = 300, + limit = 30, since = since, ), ), @@ -60,7 +60,7 @@ fun filterLongFormByCommunity( authors = authors, tags = mapOf("k" to listOf("5300"), "a" to listOf(community)), kinds = listOf(LongTextNoteEvent.KIND), - limit = 300, + limit = 30, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt index 6764bf8a9e..2fad478f3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt @@ -44,7 +44,7 @@ fun filterLongFormByGeohash( Filter( kinds = listOf(LongTextNoteEvent.KIND), tags = mapOf("g" to geoHashes), - limit = 300, + limit = 30, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt index a9c47e112a..4ef4daa208 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt @@ -44,7 +44,7 @@ fun filterLongFormByHashtag( Filter( kinds = listOf(LongTextNoteEvent.KIND), tags = mapOf("t" to hashtags), - limit = 300, + limit = 30, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt index 026affbdd7..09ad32c4aa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt @@ -39,7 +39,7 @@ fun filterLongFormGlobal( filter = Filter( kinds = listOf(LongTextNoteEvent.KIND), - limit = 300, + limit = 30, since = since, ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt index d1bb4587d3..3959aa50d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt @@ -46,7 +46,7 @@ fun filterPublicChatsGlobal( ChannelMetadataEvent.KIND, ChannelMessageEvent.KIND, ), - limit = 300, + limit = 30, since = since, ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt index 78b8ed8d7f..0feb24def2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt @@ -39,7 +39,7 @@ fun filterFollowSetsGlobal( filter = Filter( kinds = listOf(FollowListEvent.KIND), - limit = 300, + limit = 30, since = since, ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt index c49b471e3e..7252d171fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt @@ -40,7 +40,7 @@ fun filterLiveActivitiesGlobal( filter = Filter( kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), - limit = 300, + limit = 30, since = since, ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt index 513b738ecc..4a8576855a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt @@ -44,7 +44,7 @@ fun filterCommunitiesByGeohash( Filter( kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), tags = mapOf("g" to geoHashes), - limit = 300, + limit = 100, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt index bc439a1d01..13074bde1f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt @@ -45,7 +45,7 @@ fun filterCommunitiesByHashtag( Filter( kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), tags = mapOf("t" to hashtags), - limit = 300, + limit = 100, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt index 4835e9d21b..6f79e2f79c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt @@ -41,7 +41,7 @@ fun filterCommunitiesGlobal( filter = Filter( kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), - limit = 300, + limit = 30, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt index 317aa48578..00e94d9e07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt @@ -41,7 +41,7 @@ fun filterContentDVMsGlobal( Filter( kinds = listOf(AppDefinitionEvent.KIND), tags = mapOf("k" to listOf("5300")), - limit = 300, + limit = 30, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt index b711f3e0a9..41ce6ecd13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt @@ -40,7 +40,7 @@ fun filterClassifiedsGlobal( filter = Filter( kinds = listOf(ClassifiedsEvent.KIND), - limit = 300, + limit = 30, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt index 358b05eef8..245b047672 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt @@ -78,7 +78,7 @@ fun filterHomePostsFromCommunity( "a" to listOf(community), "k" to HomePostsFromCommunityKindsStr, ), - limit = 200, + limit = 100, since = since, ), ), @@ -90,7 +90,7 @@ fun filterHomePostsFromCommunity( authors = authors, tags = mapOf("a" to listOf(community)), kinds = HomePostsFromCommunityKinds, - limit = 200, + limit = 100, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt index b0915ad517..64ad5d7db6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt @@ -43,7 +43,7 @@ fun filterPictureAndVideoGeohash( Filter( kinds = PictureAndVideoKinds, tags = mapOf("g" to geoHashes), - limit = 400, + limit = 100, since = since, ), ), @@ -57,7 +57,7 @@ fun filterPictureAndVideoGeohash( "g" to geoHashes, "m" to LegacyMimeTypes, ), - limit = 200, + limit = 100, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt index 068f4869f7..3b21de472d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt @@ -44,7 +44,7 @@ fun filterPictureAndVideoHashtag( Filter( kinds = PictureAndVideoKinds, tags = mapOf("t" to hashtags), - limit = 400, + limit = 50, since = since, ), ), @@ -58,7 +58,7 @@ fun filterPictureAndVideoHashtag( "t" to hashtags, "m" to LegacyMimeTypes, ), - limit = 200, + limit = 50, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt index 674dd199d6..953a323ee0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt @@ -42,7 +42,7 @@ fun filterPictureAndVideoGlobal( filter = Filter( kinds = PictureAndVideoKinds, - limit = 200, + limit = 100, since = since, ), ), @@ -52,7 +52,7 @@ fun filterPictureAndVideoGlobal( Filter( kinds = PictureAndVideoLegacyKinds, tags = LegacyMimeTypeMap, - limit = 200, + limit = 100, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt index be5c39ef4d..ebd5b7d3c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt @@ -44,7 +44,7 @@ fun filterPictureAndVideoAuthors( Filter( authors = authorList, kinds = PictureAndVideoKinds, - limit = 400, + limit = 100, since = since, ), ), @@ -55,7 +55,7 @@ fun filterPictureAndVideoAuthors( authors = authorList, kinds = PictureAndVideoLegacyKinds, tags = LegacyMimeTypeMap, - limit = 200, + limit = 100, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt index 1db6d48f68..7f55fac630 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt @@ -52,7 +52,7 @@ fun filterPictureAndVideoAllCommunities( "a" to communityList, "k" to PictureAndVideoKTags, ), - limit = 300, + limit = 100, since = since, ), ), @@ -63,7 +63,7 @@ fun filterPictureAndVideoAllCommunities( Filter( tags = mapOf("a" to communityList), kinds = PictureAndVideoKinds, - limit = 300, + limit = 100, since = since, ), ), @@ -78,7 +78,7 @@ fun filterPictureAndVideoAllCommunities( "a" to communityList, "k" to listOf(FileHeaderEvent.KIND.toString(), FileStorageHeaderEvent.KIND.toString()), ), - limit = 300, + limit = 100, since = since, ), ), @@ -93,7 +93,7 @@ fun filterPictureAndVideoAllCommunities( "m" to LegacyMimeTypes, ), kinds = PictureAndVideoLegacyKinds, - limit = 300, + limit = 100, since = since, ), ), From 1c5acc00f347e25822d41b7f91225c9d7e4ed7f4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Jul 2025 10:11:03 -0400 Subject: [PATCH 104/185] More appropriate limits --- .../home/datasource/nip01Core/FilterHomePostsByGeohashes.kt | 2 +- .../home/datasource/nip01Core/FilterHomePostsByGlobal.kt | 4 ++-- .../home/datasource/nip22Comments/FilterPostsByScopes.kt | 2 +- .../home/datasource/nip65Follows/FilterHomePostsByAuthors.kt | 4 ++-- .../nip72Communities/FilterHomePostsByAllCommunities.kt | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt index 0bf255dce7..2238c324f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt @@ -63,7 +63,7 @@ fun filterHomePostsByGeohashes( Filter( kinds = HomePostsByGeohashKinds, tags = mapOf("g" to geotags.sorted()), - limit = 100, + limit = geotags.size * 20, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt index 1a6cc8e689..320c4cd655 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt @@ -78,7 +78,7 @@ fun filterHomePostsByGlobal( filter = Filter( kinds = HomePostsByGlobalKinds, - limit = 400, + limit = 50, since = since, ), ), @@ -87,7 +87,7 @@ fun filterHomePostsByGlobal( filter = Filter( kinds = HomePostsByGlobalKinds2, - limit = 400, + limit = 50, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt index 04172ee0db..c4ce575b19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt @@ -41,7 +41,7 @@ fun filterHomePostsByScopes( Filter( kinds = CommentKinds, tags = mapOf("I" to scopesToLoad.toList()), - limit = 100, + limit = scopesToLoad.size * 20, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt index dcc8c45442..bf6595e0d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -80,7 +80,7 @@ fun filterHomePostsByAuthors( Filter( kinds = HomePostsKinds, authors = authorList, - limit = 400, + limit = authorList.size * 10, since = since, ), ), @@ -90,7 +90,7 @@ fun filterHomePostsByAuthors( Filter( kinds = HomePostsKinds2, authors = authorList, - limit = 400, + limit = authorList.size * 10, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt index 14cf1c6758..1fb065d67e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt @@ -46,7 +46,7 @@ fun filterHomePostsFromAllCommunities( "a" to communityList, "k" to HomePostsFromCommunityKindsStr, ), - limit = 300, + limit = communityList.size * 20, since = since, ), ), @@ -57,7 +57,7 @@ fun filterHomePostsFromAllCommunities( Filter( tags = mapOf("a" to communityList), kinds = HomePostsFromCommunityKinds, - limit = 300, + limit = communityList.size * 20, since = since, ), ), From 6066c7d06dbc565a5b3180b1fdbd7ff64c759b7d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Jul 2025 17:33:31 -0400 Subject: [PATCH 105/185] Removes relay lists from notifications --- .../vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt | 4 ++++ .../loggedIn/notifications/dal/NotificationFeedFilter.kt | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt new file mode 100644 index 0000000000..8aad204ccd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt @@ -0,0 +1,4 @@ +package com.vitorpamplona.amethyst.model.nip03Timestamp + +class OtsState { +} \ No newline at end of file diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index c4b0a8d300..07efff1bb7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -123,6 +124,7 @@ class NotificationFeedFilter( it.event !is NIP90StatusEvent && it.event !is NIP90ContentDiscoveryRequestEvent && it.event !is GiftWrapEvent && + it.event !is PrivateTagArrayEvent && (it.event is LnZapEvent || notifAuthor != loggedInUserHex) && (filterParams.isGlobal(it.relays) || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor) == true) && it.event?.isTaggedUser(loggedInUserHex) ?: false && From 95c6dcce5e22a09020e084c900f87b7220ead2b2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Jul 2025 17:34:48 -0400 Subject: [PATCH 106/185] Moves Ots to its own state class --- .../vitorpamplona/amethyst/model/Account.kt | 104 +++++------------- .../amethyst/model/nip03Timestamp/OtsState.kt | 80 +++++++++++++- .../amethyst/ui/note/elements/DropDownMenu.kt | 6 +- .../ui/screen/loggedIn/AccountViewModel.kt | 28 +---- 4 files changed, 113 insertions(+), 105 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 9fbaf9fbc5..415463f732 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowsPerOutboxRelay +import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState import com.vitorpamplona.amethyst.model.nip18Reposts.RepostAction @@ -127,10 +128,8 @@ import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser -import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.references.references -import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent @@ -207,10 +206,8 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.math.BigDecimal -import java.util.Base64 import java.util.Locale @OptIn(DelicateCoroutinesApi::class) @@ -305,6 +302,15 @@ class Account( val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) + val otsResolver: OtsResolver = + OtsResolverBuilder().build( + Amethyst.instance.okHttpClients, + ::shouldUseTorForMoneyOperations, + Amethyst.instance.otsBlockHeightCache, + ) + + val otsState = OtsState(signer, cache, otsResolver, scope, settings) + val feedDecryptionCaches = FeedDecryptionCaches( peopleListCache = peopleListDecryptionCache, @@ -457,10 +463,6 @@ class Account( private suspend fun sendNewAppSpecificData() = sendMyPublicAndPrivateOutbox(appSpecific.saveNewAppSpecificData()) - suspend fun countFollowersOf(pubkey: HexKey): Int = cache.users.count { _, it -> it.latestContactList?.isTaggedUser(pubkey) ?: false } - - suspend fun followerCount(): Int = countFollowersOf(signer.pubKey) - suspend fun sendNewUserMetadata( name: String? = null, picture: String? = null, @@ -493,7 +495,7 @@ class Account( github, ) - sendEverywhere(userMetadataEvent) + sendLiterallyEverywhere(userMetadataEvent) } fun reactionTo( @@ -877,47 +879,7 @@ class Account( } } - suspend fun updateAttestations() { - Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations") - - val otsResolver = otsResolver() - - settings.pendingAttestations.value.toList().forEach { (key, value) -> - val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver) - - if (otsState != null) { - val hint = cache.getNoteIfExists(key)?.toEventHint() - val template = - if (hint != null) { - OtsEvent.build(hint, otsState) - } else { - OtsEvent.build(key, otsState) - } - - val otsEvent = signer.sign(template) - - cache.justConsumeMyOwnEvent(otsEvent) - client.send(otsEvent, computeRelayListToBroadcast(otsEvent)) - - settings.pendingAttestations.update { it - key } - } - } - } - - fun hasPendingAttestations(note: Note): Boolean { - val id = note.event?.id ?: note.idHex - return settings.pendingAttestations.value[id] != null - } - - fun timestamp(note: Note) { - if (!isWriteable()) return - if (note.isDraft()) return - - val id = note.event?.id ?: note.idHex - val otsResolver = otsResolver() - - settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id, otsResolver))) - } + suspend fun updateAttestations() = sendAutomatic(otsState.updateAttestations()) suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user)) @@ -943,6 +905,14 @@ class Account( suspend fun unfollowGeohash(geohash: String) = sendMyPublicAndPrivateOutbox(geohashList.unfollow(geohash)) + fun sendAutomatic(events: List) = events.forEach { sendAutomatic(it) } + + fun sendAutomatic(event: Event?) { + if (event == null) return + cache.justConsumeMyOwnEvent(event) + client.send(event, computeRelayListToBroadcast(event)) + } + fun sendMyPublicAndPrivateOutbox(event: Event?) { if (event == null) return client.send(event, outboxRelays.flow.value) @@ -956,7 +926,7 @@ class Account( } } - fun sendEverywhere(event: Event) { + fun sendLiterallyEverywhere(event: Event) { client.send(event, outboxRelays.flow.value + client.relayStatusFlow().value.available) cache.justConsumeMyOwnEvent(event) } @@ -1620,16 +1590,11 @@ class Account( delete(note) } - fun isInPrivateBookmarks(note: Note) = bookmarkState.isInPrivateBookmarks(note) - - fun isInPublicBookmarks(note: Note) = bookmarkState.isInPublicBookmarks(note) - suspend fun addBookmark( note: Note, isPrivate: Boolean, ) { - if (!isWriteable()) return - if (note.isDraft()) return + if (!isWriteable() || note.isDraft()) return sendMyPublicAndPrivateOutbox(bookmarkState.addBookmark(note, isPrivate)) } @@ -1638,8 +1603,7 @@ class Account( note: Note, isPrivate: Boolean, ) { - if (!isWriteable()) return - if (note.isDraft()) return + if (!isWriteable() || note.isDraft()) return val event = bookmarkState.removeBookmark(note, isPrivate) if (event != null) { @@ -1735,16 +1699,7 @@ class Account( } } - suspend fun decryptZapOrNull(event: LnZapRequestEvent): LnZapPrivateEvent? = - if (event.isPrivateZap()) { - if (isWriteable()) { - privateZapsDecryptionCache.decryptPrivateZap(event) - } else { - null - } - } else { - null - } + suspend fun decryptZapOrNull(event: LnZapRequestEvent): LnZapPrivateEvent? = if (event.isPrivateZap() && isWriteable()) privateZapsDecryptionCache.decryptPrivateZap(event) else null fun isAllHidden(users: Set): Boolean = users.all { isHidden(it) } @@ -1816,7 +1771,7 @@ class Account( ).toSet() } - suspend fun saveDMRelayList(dmRelays: List) = sendEverywhere(dmRelayList.saveRelayList(dmRelays)) + suspend fun saveDMRelayList(dmRelays: List) = sendLiterallyEverywhere(dmRelayList.saveRelayList(dmRelays)) suspend fun savePrivateOutboxRelayList(relays: List) = sendMyPublicAndPrivateOutbox(privateStorageRelayList.saveRelayList(relays)) @@ -1826,7 +1781,7 @@ class Account( suspend fun saveBlockedRelayList(blockedRelays: List) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays)) - suspend fun sendNip65RelayList(relays: List) = sendEverywhere(nip65RelayList.saveRelayList(relays)) + suspend fun sendNip65RelayList(relays: List) = sendLiterallyEverywhere(nip65RelayList.saveRelayList(relays)) suspend fun sendFileServersList(servers: List) = sendMyPublicAndPrivateOutbox(fileStorageServers.saveFileServersList(servers)) @@ -1965,13 +1920,6 @@ class Account( TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) } - fun otsResolver(): OtsResolver = - OtsResolverBuilder().build( - Amethyst.instance.okHttpClients, - ::shouldUseTorForMoneyOperations, - Amethyst.instance.otsBlockHeightCache, - ) - init { Log.d("AccountRegisterObservers", "Init") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt index 8aad204ccd..961ff0b7f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt @@ -1,4 +1,80 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ package com.vitorpamplona.amethyst.model.nip03Timestamp -class OtsState { -} \ No newline at end of file +import android.util.Log +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.update +import java.util.Base64 + +class OtsState( + val signer: NostrSigner, + val cache: LocalCache, + val otsResolver: OtsResolver, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + suspend fun updateAttestations(): List { + Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations") + + return settings.pendingAttestations.value.toList().mapNotNull { (key, value) -> + val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver) + + if (otsState != null) { + val hint = cache.getNoteIfExists(key)?.toEventHint() + val template = + if (hint != null) { + OtsEvent.build(hint, otsState) + } else { + OtsEvent.build(key, otsState) + } + + val otsEvent = signer.sign(template) + + settings.pendingAttestations.update { it - key } + + otsEvent + } else { + null + } + } + } + + fun hasPendingAttestations(note: Note): Boolean { + val id = note.event?.id ?: note.idHex + return settings.pendingAttestations.value[id] != null + } + + fun timestamp(note: Note) { + if (note.isDraft()) return + + val id = note.event?.id ?: note.idHex + + settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id, otsResolver))) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index a75771bb55..ef1a115562 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -273,7 +273,7 @@ fun NoteDropDownMenu( }, ) HorizontalDivider(thickness = DividerThickness) - if (accountViewModel.account.hasPendingAttestations(note)) { + if (accountViewModel.account.otsState.hasPendingAttestations(note)) { DropdownMenuItem( text = { Text(stringRes(R.string.timestamp_pending)) }, onClick = { @@ -363,8 +363,8 @@ fun WatchBookmarksFollowsAndAccount( val newState = DropDownParams( isFollowingAuthor = accountViewModel.isFollowing(note.author), - isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note), - isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note), + isPrivateBookmarkNote = accountViewModel.account.bookmarkState.isInPrivateBookmarks(note), + isPublicBookmarkNote = accountViewModel.account.bookmarkState.isInPublicBookmarks(note), isLoggedUser = accountViewModel.isLoggedUser(note.author), isSensitive = note.event?.isSensitiveOrNSFW() ?: false, showSensitiveContent = showSensitiveContent, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 883571d996..bbf0cee47d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -741,33 +741,17 @@ class AccountViewModel( fun bookmarks(user: User): Note = LocalCache.getOrCreateAddressableNote(BookmarkListEvent.createBookmarkAddress(user.pubkeyHex)) - fun addPrivateBookmark(note: Note) { - runIOCatching { account.addBookmark(note, true) } - } + fun addPrivateBookmark(note: Note) = runIOCatching { account.addBookmark(note, true) } - fun addPublicBookmark(note: Note) { - runIOCatching { account.addBookmark(note, false) } - } + fun addPublicBookmark(note: Note) = runIOCatching { account.addBookmark(note, false) } - fun removePrivateBookmark(note: Note) { - runIOCatching { account.removeBookmark(note, true) } - } + fun removePrivateBookmark(note: Note) = runIOCatching { account.removeBookmark(note, true) } - fun removePublicBookmark(note: Note) { - runIOCatching { account.removeBookmark(note, false) } - } + fun removePublicBookmark(note: Note) = runIOCatching { account.removeBookmark(note, false) } - fun isInPrivateBookmarks(note: Note): Boolean = account.isInPrivateBookmarks(note) + fun broadcast(note: Note) = runIOCatching { account.broadcast(note) } - fun isInPublicBookmarks(note: Note): Boolean = account.isInPublicBookmarks(note) - - fun broadcast(note: Note) { - runIOCatching { account.broadcast(note) } - } - - fun timestamp(note: Note) { - runIOCatching { account.timestamp(note) } - } + fun timestamp(note: Note) = runIOCatching { account.otsState.timestamp(note) } var lastTimeItTriedToUpdateAttestations: Long = 0 From 8e6d71e4223235846394df7476c6fce98b51fe6f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 21 Jul 2025 19:09:06 -0400 Subject: [PATCH 107/185] Minimizes the dependency on the Account further by moving Tor settings to a tor Privacy State --- .../vitorpamplona/amethyst/model/Account.kt | 213 +++--------------- .../amethyst/model/LocalCache.kt | 4 +- .../com/vitorpamplona/amethyst/model/Note.kt | 6 +- .../amethyst/model/nip03Timestamp/OtsState.kt | 17 +- .../model/privacyOptions/PrivacyState.kt | 125 ++++++++++ .../model/privateChats/ChatroomList.kt | 4 - .../amethyst/model/torState/TorRelayState.kt | 2 + ...Builder.kt => OkHttpOtsResolverBuilder.kt} | 31 +-- .../service/uploads/UploadOrchestrator.kt | 8 +- .../ui/actions/NewUserMetadataScreen.kt | 5 +- .../ui/actions/NewUserMetadataViewModel.kt | 54 +++-- .../ui/navigation/routes/RouteMaker.kt | 6 +- .../amethyst/ui/note/ReactionsRow.kt | 2 +- .../nip22Comments/CommentPostViewModel.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 83 ++----- .../loggedIn/DecryptAndIndexProcessor.kt | 2 +- .../ui/screen/loggedIn/LoggedInPage.kt | 2 +- .../privateDM/send/ChatNewMessageViewModel.kt | 2 +- .../metadata/ChannelMetadataViewModel.kt | 4 +- .../send/ChannelNewMessageViewModel.kt | 2 +- .../nip99Classifieds/NewProductViewModel.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 2 +- .../loggedIn/profile/header/MessageButton.kt | 12 +- .../relays/common/BasicRelaySetupInfoModel.kt | 2 +- .../relays/nip65/Nip65RelayListViewModel.kt | 4 +- .../quartz/nip57Zaps/PrivateZapTests.kt | 4 +- .../nip03Timestamp/OtsResolverBuilder.kt | 25 ++ .../nip03Timestamp/VerificationStateCache.kt | 8 +- .../quartz/nip57Zaps/LnZapRequestEvent.kt | 14 +- 29 files changed, 305 insertions(+), 342 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/{OtsResolverBuilder.kt => OkHttpOtsResolverBuilder.kt} (69%) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 415463f732..8f233b3b65 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState import com.vitorpamplona.amethyst.model.nip78AppSpecific.AppSpecificState import com.vitorpamplona.amethyst.model.nip96FileStorage.FileStorageServerListState import com.vitorpamplona.amethyst.model.nipB7Blossom.BlossomServerListState +import com.vitorpamplona.amethyst.model.privacyOptions.PrivacyState import com.vitorpamplona.amethyst.model.serverList.MergedFollowListsState import com.vitorpamplona.amethyst.model.serverList.MergedFollowPlusMineRelayListsState import com.vitorpamplona.amethyst.model.serverList.MergedServerListState @@ -79,9 +80,8 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.OutboxLoaderState import com.vitorpamplona.amethyst.model.torState.TorRelayState import com.vitorpamplona.amethyst.service.location.LocationState -import com.vitorpamplona.amethyst.service.ots.OtsResolverBuilder +import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder import com.vitorpamplona.amethyst.service.uploads.FileHeader -import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent @@ -118,7 +118,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.downloadFirst import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedAddresses @@ -130,7 +129,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.hasAnyTaggedUser import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUserIds import com.vitorpamplona.quartz.nip01Core.tags.references.references -import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import com.vitorpamplona.quartz.nip04Dm.PrivateDMCache import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip04Dm.messages.reply @@ -302,14 +300,17 @@ class Account( val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) - val otsResolver: OtsResolver = - OtsResolverBuilder().build( + val privacyState = PrivacyState(settings) + val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope) + + val otsResolverBuilder: OkHttpOtsResolverBuilder = + OkHttpOtsResolverBuilder( Amethyst.instance.okHttpClients, - ::shouldUseTorForMoneyOperations, + privacyState::shouldUseTorForMoneyOperations, Amethyst.instance.otsBlockHeightCache, ) - val otsState = OtsState(signer, cache, otsResolver, scope, settings) + val otsState = OtsState(signer, cache, otsResolverBuilder, scope, settings) val feedDecryptionCaches = FeedDecryptionCaches( @@ -389,8 +390,6 @@ class Account( */ - val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope) - fun isWriteable(): Boolean = settings.isWriteable() suspend fun updateWarnReports(warnReports: Boolean): Boolean { @@ -463,67 +462,15 @@ class Account( private suspend fun sendNewAppSpecificData() = sendMyPublicAndPrivateOutbox(appSpecific.saveNewAppSpecificData()) - suspend fun sendNewUserMetadata( - name: String? = null, - picture: String? = null, - banner: String? = null, - website: String? = null, - pronouns: String? = null, - about: String? = null, - nip05: String? = null, - lnAddress: String? = null, - lnURL: String? = null, - twitter: String? = null, - mastodon: String? = null, - github: String? = null, - ) { - if (!isWriteable()) return - - val userMetadataEvent = - userMetadata.sendNewUserMetadata( - name, - picture, - banner, - website, - pronouns, - about, - nip05, - lnAddress, - lnURL, - twitter, - mastodon, - github, - ) - - sendLiterallyEverywhere(userMetadataEvent) - } - - fun reactionTo( - note: Note, - reaction: String, - ): List = note.reactedBy(userProfile(), reaction) - - fun hasBoosted(note: Note): Boolean = boostsTo(note).isNotEmpty() - - fun boostsTo(note: Note): List = note.boostedBy(userProfile()) - - fun hasReacted( - note: Note, - reaction: String, - ): Boolean = note.hasReacted(userProfile(), reaction) - suspend fun reactTo( note: Note, reaction: String, ) = ReactionAction.reactTo( - note, - reaction, - userProfile(), - signer, - onPublic = { - client.send(it, computeMyReactionToNote(note, it)) - cache.justConsumeMyOwnEvent(it) - }, + note = note, + reaction = reaction, + by = userProfile(), + signer = signer, + onPublic = ::sendAutomatic, onPrivate = ::broadcastPrivately, ) @@ -534,18 +481,15 @@ class Account( zapType: LnZapEvent.ZapType, toUser: User?, additionalRelays: Set? = null, - ): LnZapRequestEvent { - val relays = nip65RelayList.inboxFlow.value + (additionalRelays ?: emptySet()) - return LnZapRequestEvent.create( - event, - relays = relays.mapTo(mutableSetOf()) { it.url }, - signer, - pollOption, - message, - zapType, - toUser?.pubkeyHex, - ) - } + ) = LnZapRequestEvent.create( + zappedEvent = event, + relays = nip65RelayList.inboxFlow.value + (additionalRelays ?: emptySet()), + signer = signer, + pollOption = pollOption, + message = message, + zapType = zapType, + toUserPubHex = toUser?.pubkeyHex, + ) suspend fun calculateIfNoteWasZappedByAccount( zappedNote: Note?, @@ -570,12 +514,10 @@ class Account( message: String = "", zapType: LnZapEvent.ZapType, ): LnZapRequestEvent { - val relays = nip65RelayList.inboxFlow.value + user.inboxRelays() - val zapRequest = LnZapRequestEvent.create( userHex = user.pubkeyHex, - relays = relays, + relays = nip65RelayList.inboxFlow.value + user.inboxRelays(), signer = signer, message = message, zapType = zapType, @@ -596,9 +538,7 @@ class Account( type: ReportType, ) = sendMyPublicAndPrivateOutbox(ReportAction.report(user, type, userProfile(), signer)) - suspend fun delete(note: Note) { - delete(listOf(note)) - } + suspend fun delete(note: Note) = delete(listOf(note)) suspend fun delete(notes: List) { if (!isWriteable()) return @@ -627,10 +567,7 @@ class Account( if (!isWriteable()) return if (event.pubKey != signer.pubKey) return - val deletionEvent = - signer.sign( - DeletionEvent.build(listOf(event)), - ) + val deletionEvent = signer.sign(DeletionEvent.build(listOf(event))) client.send(deletionEvent, outboxRelays.flow.value + additionalRelays) cache.justConsumeMyOwnEvent(deletionEvent) } @@ -1822,104 +1759,6 @@ class Account( fun markDonatedInThisVersion() = settings.markDonatedInThisVersion(BuildConfig.VERSION_NAME) - fun shouldUseTorForImageDownload(url: String) = - shouldUseTorFor( - url, - settings.torSettings.torType.value, - settings.torSettings.imagesViaTor.value, - ) - - fun shouldUseTorFor( - url: String, - torType: TorType, - imagesViaTor: Boolean, - ) = when (torType) { - TorType.OFF -> false - TorType.INTERNAL -> shouldUseTor(url, imagesViaTor) - TorType.EXTERNAL -> shouldUseTor(url, imagesViaTor) - } - - private fun shouldUseTor( - normalizedUrl: String, - final: Boolean, - ): Boolean = - if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) { - false - } else if (RelayUrlNormalizer.isOnion(normalizedUrl)) { - true - } else { - final - } - - fun shouldUseTorForVideoDownload() = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> settings.torSettings.videosViaTor.value - TorType.EXTERNAL -> settings.torSettings.videosViaTor.value - } - - fun shouldUseTorForVideoDownload(url: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value) - } - - fun shouldUseTorForPreviewUrl(url: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value) - } - - fun shouldUseTorForTrustedRelays() = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> settings.torSettings.trustedRelaysViaTor.value - TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value - } - - fun shouldUseTorForClean(relay: NormalizedRelayUrl) = torRelayState.flow.value.useTor(relay) - - private fun checkLocalHostOnionAndThen( - url: String, - final: Boolean, - ): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final) - - private fun checkLocalHostOnionAndThen( - normalizedUrl: String, - isOnionRelaysActive: Boolean, - final: Boolean, - ): Boolean = - if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) { - false - } else if (RelayUrlNormalizer.isOnion(normalizedUrl)) { - isOnionRelaysActive - } else { - final - } - - fun shouldUseTorForMoneyOperations(url: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value) - } - - fun shouldUseTorForNIP05(url: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value) - } - - fun shouldUseTorForNIP96(url: String) = - when (settings.torSettings.torType.value) { - TorType.OFF -> false - TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) - TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) - } - init { Log.d("AccountRegisterObservers", "Init") diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 2bf690c518..36bfec4286 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -76,7 +76,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUsers import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder import com.vitorpamplona.quartz.nip03Timestamp.VerificationState import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent @@ -2102,7 +2102,7 @@ object LocalCache : ILocalCache { suspend fun findEarliestOtsForNote( note: Note, - resolverBuilder: () -> OtsResolver, + resolverBuilder: OtsResolverBuilder, ): Long? { checkNotInMainThread() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 5f5d3fbd94..204d98c5b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -745,14 +745,14 @@ open class Note( fun hasReacted( loggedIn: User, content: String, - ): Boolean = reactedBy(loggedIn, content).isNotEmpty() + ): Boolean = allReactionsOfContentByAuthor(loggedIn, content).isNotEmpty() - fun reactedBy( + fun allReactionsOfContentByAuthor( loggedIn: User, content: String, ): List = reactions[content]?.filter { it.author == loggedIn } ?: emptyList() - fun reactedBy(loggedIn: User): List = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key } + fun allReactionsByAuthor(loggedIn: User): List = reactions.filter { it.value.any { it.author == loggedIn } }.mapNotNull { it.key } fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { val fiveMinsAgo = TimeUtils.fiveMinutesAgo() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt index 961ff0b7f0..44f316caad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsState.kt @@ -24,10 +24,10 @@ import android.util.Log import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.update import java.util.Base64 @@ -35,7 +35,7 @@ import java.util.Base64 class OtsState( val signer: NostrSigner, val cache: LocalCache, - val otsResolver: OtsResolver, + val otsResolver: OkHttpOtsResolverBuilder, val scope: CoroutineScope, val settings: AccountSettings, ) { @@ -43,7 +43,7 @@ class OtsState( Log.d("Pending Attestations", "Updating ${settings.pendingAttestations.value.size} pending attestations") return settings.pendingAttestations.value.toList().mapNotNull { (key, value) -> - val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver) + val otsState = OtsEvent.upgrade(Base64.getDecoder().decode(value), key, otsResolver.build()) if (otsState != null) { val hint = cache.getNoteIfExists(key)?.toEventHint() @@ -75,6 +75,15 @@ class OtsState( val id = note.event?.id ?: note.idHex - settings.addPendingAttestation(id, Base64.getEncoder().encodeToString(OtsEvent.stamp(id, otsResolver))) + settings.addPendingAttestation( + id = id, + stamp = + Base64.getEncoder().encodeToString( + OtsEvent.stamp( + id, + otsResolver.build(), + ), + ), + ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt new file mode 100644 index 0000000000..dcbee7e43d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privacyOptions/PrivacyState.kt @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.privacyOptions + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.ui.tor.TorType +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer + +class PrivacyState( + val settings: AccountSettings, +) { + fun shouldUseTorForImageDownload(url: String) = + shouldUseTorFor( + url, + settings.torSettings.torType.value, + settings.torSettings.imagesViaTor.value, + ) + + fun shouldUseTorFor( + url: String, + torType: TorType, + imagesViaTor: Boolean, + ) = when (torType) { + TorType.OFF -> false + TorType.INTERNAL -> shouldUseTor(url, imagesViaTor) + TorType.EXTERNAL -> shouldUseTor(url, imagesViaTor) + } + + private fun shouldUseTor( + normalizedUrl: String, + final: Boolean, + ): Boolean = + if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) { + false + } else if (RelayUrlNormalizer.isOnion(normalizedUrl)) { + true + } else { + final + } + + fun shouldUseTorForVideoDownload() = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> settings.torSettings.videosViaTor.value + TorType.EXTERNAL -> settings.torSettings.videosViaTor.value + } + + fun shouldUseTorForVideoDownload(url: String) = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.videosViaTor.value) + } + + fun shouldUseTorForPreviewUrl(url: String) = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.urlPreviewsViaTor.value) + } + + fun shouldUseTorForTrustedRelays() = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> settings.torSettings.trustedRelaysViaTor.value + TorType.EXTERNAL -> settings.torSettings.trustedRelaysViaTor.value + } + + private fun checkLocalHostOnionAndThen( + url: String, + final: Boolean, + ): Boolean = checkLocalHostOnionAndThen(url, settings.torSettings.onionRelaysViaTor.value, final) + + private fun checkLocalHostOnionAndThen( + normalizedUrl: String, + isOnionRelaysActive: Boolean, + final: Boolean, + ): Boolean = + if (RelayUrlNormalizer.isLocalHost(normalizedUrl)) { + false + } else if (RelayUrlNormalizer.isOnion(normalizedUrl)) { + isOnionRelaysActive + } else { + final + } + + fun shouldUseTorForMoneyOperations(url: String) = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.moneyOperationsViaTor.value) + } + + fun shouldUseTorForNIP05(url: String) = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip05VerificationsViaTor.value) + } + + fun shouldUseTorForUploads(url: String) = + when (settings.torSettings.torType.value) { + TorType.OFF -> false + TorType.INTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) + TorType.EXTERNAL -> checkLocalHostOnionAndThen(url, settings.torSettings.nip96UploadsViaTor.value) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt index 797070e577..f73f4790e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt @@ -65,10 +65,6 @@ class ChatroomList( } } - fun createChatroom(withKey: ChatroomKey) { - getOrCreatePrivateChatroom(withKey) - } - fun removeMessage( user: User, msg: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt index db68542b43..f1190941eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/TorRelayState.kt @@ -115,4 +115,6 @@ class TorRelayState( dmRelayList = dmRelayState.flow.value, ), ) + + fun shouldUseTorForClean(relay: NormalizedRelayUrl) = flow.value.useTor(relay) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsResolverBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpOtsResolverBuilder.kt similarity index 69% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsResolverBuilder.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpOtsResolverBuilder.kt index 5925ea6572..d858cdd6b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OtsResolverBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ots/OkHttpOtsResolverBuilder.kt @@ -22,8 +22,13 @@ package com.vitorpamplona.amethyst.service.ots import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver +import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder -class OtsResolverBuilder { +class OkHttpOtsResolverBuilder( + val okHttpClients: DualHttpClientManager, + val shouldUseTorForUrl: (String) -> Boolean, + val cache: OtsBlockHeightCache, +) : OtsResolverBuilder { fun getAPI(usingTor: Boolean) = if (usingTor) { OkHttpBitcoinExplorer.MEMPOOL_API_URL @@ -31,22 +36,20 @@ class OtsResolverBuilder { OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL } - fun build( - okHttpClients: DualHttpClientManager, - shouldUseTorForUrl: (String) -> Boolean, - cache: OtsBlockHeightCache, - ): OtsResolver { + override fun build(): OtsResolver { val shouldUseTor = shouldUseTorForUrl(OkHttpBitcoinExplorer.MEMPOOL_API_URL) return OtsResolver( - OkHttpBitcoinExplorer( - getAPI(shouldUseTor), - okHttpClients.getHttpClient(shouldUseTor), - cache, - ), - OkHttpCalendarBuilder { - okHttpClients.getHttpClient(shouldUseTorForUrl(it)) - }, + explorer = + OkHttpBitcoinExplorer( + baseAPI = getAPI(usingTor = shouldUseTor), + client = okHttpClients.getHttpClient(shouldUseTor), + cache = cache, + ), + calendarBuilder = + OkHttpCalendarBuilder { + okHttpClients.getHttpClient(shouldUseTorForUrl(it)) + }, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt index c6859a460c..4423bd16c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/UploadOrchestrator.kt @@ -152,7 +152,7 @@ class UploadOrchestrator { alt = alt, sensitiveContent = contentWarningReason, serverBaseUrl = serverBaseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, onProgress = { percent: Float -> updateState(0.2 + (0.2 * percent), UploadingState.Uploading) }, @@ -165,7 +165,7 @@ class UploadOrchestrator { localContentType = contentType, originalContentType = contentTypeForResult, originalHash = originalHash, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, ) } catch (_: SignerExceptions.ReadOnlyException) { error(R.string.login_with_a_private_key_to_be_able_to_upload) @@ -198,7 +198,7 @@ class UploadOrchestrator { alt = alt, sensitiveContent = contentWarningReason, serverBaseUrl = serverBaseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, httpAuth = account::createBlossomUploadAuth, context = context, ) @@ -206,7 +206,7 @@ class UploadOrchestrator { verifyHeader( uploadResult = result, localContentType = contentType, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, originalHash = originalHash, originalContentType = contentTypeForResult, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt index ddb143880f..6d8b8c1f58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt @@ -58,10 +58,11 @@ fun NewUserMetadataScreen( accountViewModel: AccountViewModel, ) { val postViewModel: NewUserMetadataViewModel = viewModel() + postViewModel.init(accountViewModel) val context = LocalContext.current - LaunchedEffect(Unit) { - postViewModel.load(accountViewModel.account) + LaunchedEffect(accountViewModel) { + postViewModel.load() } Scaffold( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index c24c121cd4..5d0bc32227 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -25,7 +25,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account @@ -35,17 +34,17 @@ import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlin.coroutines.cancellation.CancellationException class NewUserMetadataViewModel : ViewModel() { + private lateinit var accountViewModel: AccountViewModel private lateinit var account: Account // val userName = mutableStateOf("") @@ -68,9 +67,12 @@ class NewUserMetadataViewModel : ViewModel() { var isUploadingImageForPicture by mutableStateOf(false) var isUploadingImageForBanner by mutableStateOf(false) - fun load(account: Account) { - this.account = account + fun init(accountViewModel: AccountViewModel) { + this.accountViewModel = accountViewModel + this.account = accountViewModel.account + } + fun load() { account.userProfile().let { // userName.value = it.bestUsername() ?: "" displayName.value = it.info?.bestName() ?: "" @@ -100,21 +102,25 @@ class NewUserMetadataViewModel : ViewModel() { fun create() { // Tries to not delete any existing attribute that we do not work with. - viewModelScope.launch(Dispatchers.IO) { - account.sendNewUserMetadata( - name = displayName.value, - picture = picture.value, - banner = banner.value, - website = website.value, - pronouns = pronouns.value, - about = about.value, - nip05 = nip05.value, - lnAddress = lnAddress.value, - lnURL = lnURL.value, - twitter = twitter.value, - mastodon = mastodon.value, - github = github.value, - ) + accountViewModel.runIOCatching { + val metadata = + account.userMetadata.sendNewUserMetadata( + name = displayName.value, + picture = picture.value, + banner = banner.value, + website = website.value, + pronouns = pronouns.value, + about = about.value, + nip05 = nip05.value, + lnAddress = lnAddress.value, + lnURL = lnURL.value, + twitter = twitter.value, + mastodon = mastodon.value, + github = github.value, + ) + + account.sendLiterallyEverywhere(metadata) + clear() } } @@ -139,7 +145,7 @@ class NewUserMetadataViewModel : ViewModel() { context: Context, onError: (String, String) -> Unit, ) { - viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { upload( uri, context, @@ -155,7 +161,7 @@ class NewUserMetadataViewModel : ViewModel() { context: Context, onError: (String, String) -> Unit, ) { - viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { upload( uri, context, @@ -187,7 +193,7 @@ class NewUserMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, onProgress = {}, httpAuth = account::createHTTPAuthorization, context = context, @@ -200,7 +206,7 @@ class NewUserMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, httpAuth = account::createBlossomUploadAuth, context = context, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index dc1260c58c..cd5fbcf93c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -79,7 +79,7 @@ fun routeFor( } } else if (innerEvent is ChatroomKeyable) { val room = innerEvent.chatroomKey(loggedIn.userProfile().pubkeyHex) - loggedIn.chatroomList.createChatroom(room) + loggedIn.chatroomList.getOrCreatePrivateChatroom(room) return Route.Room(room) } else if (innerEvent is AddressableEvent) { return Route.Note(noteEvent.aTag().toTag()) @@ -104,7 +104,7 @@ fun routeFor( } } else if (noteEvent is ChatroomKeyable) { val room = noteEvent.chatroomKey(loggedIn.userProfile().pubkeyHex) - loggedIn.chatroomList.createChatroom(room) + loggedIn.chatroomList.getOrCreatePrivateChatroom(room) return Route.Room(room) } else if (noteEvent is CommunityDefinitionEvent) { return Route.Community(noteEvent.kind, noteEvent.pubKey, noteEvent.dTag()) @@ -169,7 +169,7 @@ fun routeToMessage( draftId: HexKey? = null, account: Account, ): Route { - account.chatroomList.createChatroom(room) + account.chatroomList.getOrCreatePrivateChatroom(room) return Route.Room(room, draftMessage, replyId, draftId) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 303b2ea967..b40b7357bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -1355,7 +1355,7 @@ fun ReactionChoicePopup( val iconSizePx = with(LocalDensity.current) { -iconSize.toPx().toInt() } val reactions by accountViewModel.reactionChoicesFlow().collectAsStateWithLifecycle() - val toRemove = remember { baseNote.reactedBy(accountViewModel.userProfile()).toImmutableSet() } + val toRemove = remember { baseNote.allReactionsByAuthor(accountViewModel.userProfile()).toImmutableSet() } Popup( alignment = Alignment.BottomCenter, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 3e763cb79a..61cf447429 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -623,7 +623,7 @@ open class CommentPostViewModel : iMetaAttachments.downloadAndPrepare(item.link.url) { Amethyst.Companion.instance.okHttpClients .getHttpClient( - accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) + accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index bbf0cee47d..be94cc1c71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -98,7 +98,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -266,13 +265,6 @@ class AccountViewModel( fun userProfile(): User = account.userProfile() - suspend fun reactTo( - note: Note, - reaction: String, - ) { - account.reactTo(note, reaction) - } - fun observeByETag( kind: Int, eTag: HexKey, @@ -287,8 +279,8 @@ class AccountViewModel( note: Note, reaction: String, ) { - viewModelScope.launch(Dispatchers.IO) { - val currentReactions = account.reactionTo(note, reaction) + runIOCatching { + val currentReactions = note.allReactionsOfContentByAuthor(userProfile(), reaction) if (currentReactions.isNotEmpty()) { account.delete(currentReactions) } else { @@ -298,14 +290,8 @@ class AccountViewModel( } fun reactToOrDelete(note: Note) { - viewModelScope.launch(Dispatchers.IO) { - val reaction = reactionChoices().first() - if (hasReactedTo(note, reaction)) { - deleteReactionTo(note, reaction) - } else { - reactTo(note, reaction) - } - } + val reaction = reactionChoices().first() + reactToOrDelete(note, reaction) } @Immutable @@ -402,24 +388,6 @@ class AccountViewModel( noteMustShowExpandButtonFlows.put(note, it) } - fun hasReactedTo( - baseNote: Note, - reaction: String, - ): Boolean = account.hasReacted(baseNote, reaction) - - suspend fun deleteReactionTo( - note: Note, - reaction: String, - ) { - account.delete(account.reactionTo(note, reaction)) - } - - fun hasBoosted(baseNote: Note): Boolean = account.hasBoosted(baseNote) - - fun deleteBoostsTo(note: Note) { - runIOCatching { account.delete(account.boostsTo(note)) } - } - suspend fun calculateIfNoteWasZappedByAccount( zappedNote: Note, onWasZapped: (Boolean) -> Unit, @@ -977,7 +945,7 @@ class AccountViewModel( .verifyNip05( nip05, okHttpClient = { - app.okHttpClients.getHttpClient(account.shouldUseTorForNIP05(it)) + app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForNIP05(it)) }, onSuccess = { // Marks user as verified @@ -1103,7 +1071,7 @@ class AccountViewModel( ) { onResult( withContext(Dispatchers.Default) { - LocalCache.findEarliestOtsForNote(note, account::otsResolver) + LocalCache.findEarliestOtsForNote(note, account.otsResolverBuilder) }, ) } @@ -1228,21 +1196,7 @@ class AccountViewModel( } } - fun createChatRoomFor( - user: User, - then: (ChatroomKey) -> Unit, - ) { - viewModelScope.launch(Dispatchers.IO) { - val withKey = ChatroomKey(persistentSetOf(user.pubkeyHex)) - account.chatroomList.createChatroom(withKey) - then(withKey) - } - } - - fun setTorSettings(newTorSettings: TorSettings) = - viewModelScope.launch(Dispatchers.IO) { - account.settings.setTorSettings(newTorSettings) - } + fun setTorSettings(newTorSettings: TorSettings) = runIOCatching { account.settings.setTorSettings(newTorSettings) } class Factory( val accountSettings: AccountSettings, @@ -1357,8 +1311,11 @@ class AccountViewModel( } if (isWriteable()) { - if (hasBoosted(baseNote)) { - deleteBoostsTo(baseNote) + val boosts = baseNote.boostedBy(userProfile()) + if (boosts.isNotEmpty()) { + runIOCatching { + account.delete(boosts) + } } else { onMore() } @@ -1496,21 +1453,21 @@ class AccountViewModel( } } - fun proxyPortFor(url: String): Int? = app.okHttpClients.getCurrentProxyPort(account.shouldUseTorForVideoDownload(url)) + fun proxyPortFor(url: String): Int? = app.okHttpClients.getCurrentProxyPort(account.privacyState.shouldUseTorForVideoDownload(url)) - fun okHttpClientForNip96(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(url)) + fun okHttpClientForNip96(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(url)) - fun okHttpClientForImage(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForImageDownload(url)) + fun okHttpClientForImage(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForImageDownload(url)) - fun okHttpClientForVideo(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForVideoDownload(url)) + fun okHttpClientForVideo(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForVideoDownload(url)) - fun okHttpClientForMoney(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForMoneyOperations(url)) + fun okHttpClientForMoney(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForMoneyOperations(url)) - fun okHttpClientForPreview(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForPreviewUrl(url)) + fun okHttpClientForPreview(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForPreviewUrl(url)) - fun okHttpClientForClean(url: NormalizedRelayUrl): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForClean(url)) + fun okHttpClientForClean(url: NormalizedRelayUrl): OkHttpClient = app.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(url)) - fun okHttpClientForTrustedRelays(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.shouldUseTorForTrustedRelays()) + fun okHttpClientForTrustedRelays(url: String): OkHttpClient = app.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForTrustedRelays()) fun dataSources() = app.sources diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 2343b07b7c..13f513dd29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -85,7 +85,7 @@ class DecryptAndIndexProcessor( is OtsEvent -> { // verifies new OTS upon arrival - Amethyst.instance.otsVerifCache.cacheVerify(event, account::otsResolver) + Amethyst.instance.otsVerifCache.cacheVerify(event, account.otsResolverBuilder) } is DraftEvent -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt index 39312e94d7..b5e831e9bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/LoggedInPage.kt @@ -130,7 +130,7 @@ fun SetProxyDeterminator(accountViewModel: AccountViewModel) { @Composable fun ObserveImageLoadingTor(accountViewModel: AccountViewModel) { LaunchedEffect(accountViewModel) { - Amethyst.instance.setImageLoader(accountViewModel.account::shouldUseTorForImageDownload) + Amethyst.instance.setImageLoader(accountViewModel.account.privacyState::shouldUseTorForImageDownload) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 2001c2969c..f2503744d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -640,7 +640,7 @@ class ChatNewMessageViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.okHttpClients.getHttpClient(accountViewModel.account.shouldUseTorForImageDownload(item.link.url)) + Amethyst.instance.okHttpClients.getHttpClient(accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 403dc47a4c..2995195576 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -210,7 +210,7 @@ class ChannelMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, onProgress = {}, httpAuth = account::createHTTPAuthorization, context = context, @@ -223,7 +223,7 @@ class ChannelMetadataViewModel : ViewModel() { alt = null, sensitiveContent = null, serverBaseUrl = account.settings.defaultFileServer.baseUrl, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForNIP96(it)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.privacyState.shouldUseTorForUploads(it)) }, httpAuth = account::createBlossomUploadAuth, context = context, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 0468e54346..5f04be6ed3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -588,7 +588,7 @@ open class ChannelNewMessageViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaAttachments.downloadAndPrepare( item.link.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index b1108a34ca..33b35373a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -541,7 +541,7 @@ open class NewProductViewModel : viewModelScope.launch(Dispatchers.IO) { iMetaDescription.downloadAndPrepare( item.link.url, - { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) ?: false) }, + { Amethyst.instance.okHttpClients.getHttpClient(accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 096b5ea761..49cb45da9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -786,7 +786,7 @@ open class ShortNotePostViewModel : item.link.url, ) { Amethyst.instance.okHttpClients.getHttpClient( - accountViewModel?.account?.shouldUseTorForImageDownload(item.link.url) + accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) ?: false, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt index f2a477963d..16bf12de6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextFieldDefaults.contentPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -36,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.ZeroPadding +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey @Composable fun MessageButton( @@ -49,12 +51,10 @@ fun MessageButton( .padding(horizontal = 3.dp) .width(50.dp), onClick = { - accountViewModel.createChatRoomFor(user) { chatroomKey -> - nav.nav( - routeToMessage( - room = chatroomKey, - account = accountViewModel.account, - ), + nav.nav { + routeToMessage( + room = ChatroomKey(setOf(user.pubkeyHex)), + account = accountViewModel.account, ) } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index fabec89a73..9f84c96d5a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -72,7 +72,7 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { Nip11CachedRetriever.loadRelayInfo( relay = item.relay, okHttpClient = { - Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) + Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) }, onInfo = { togglePaidRelay(item, it.limitation?.payment_required ?: false) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index a90f5aebed..c2d82c8c8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -93,7 +93,7 @@ class Nip65RelayListViewModel : ViewModel() { _homeRelays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( relay = item.relay, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) }, onInfo = { toggleHomePaidRelay(item, it.limitation?.payment_required ?: false) }, @@ -104,7 +104,7 @@ class Nip65RelayListViewModel : ViewModel() { _notificationRelays.value.forEach { item -> Nip11CachedRetriever.loadRelayInfo( relay = item.relay, - okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.shouldUseTorForClean(item.relay)) }, + okHttpClient = { Amethyst.instance.okHttpClients.getHttpClient(account.torRelayState.shouldUseTorForClean(item.relay)) }, onInfo = { toggleNotifPaidRelay(item, it.limitation?.payment_required ?: false) }, diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt index cd3056d962..70403da0ec 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt @@ -92,7 +92,7 @@ class PrivateZapTests { val privateZapRequest = runBlocking { LnZapRequestEvent.create( - originalNote = poll, + zappedEvent = poll, relays = setOf("wss://relay.damus.io/"), signer = loggedIn, pollOption = 0, @@ -152,7 +152,7 @@ class PrivateZapTests { val privateZapRequest = runBlocking { LnZapRequestEvent.create( - originalNote = textNote, + zappedEvent = textNote, relays = setOf("wss://relay.damus.io/", "wss://relay.damus2.io/", "wss://relay.damus3.io/"), signer = loggedIn, pollOption = null, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt new file mode 100644 index 0000000000..51c7f616d9 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/OtsResolverBuilder.kt @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip03Timestamp + +interface OtsResolverBuilder { + fun build(): OtsResolver +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt index 4b74a9a181..3e59d83bdb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip03Timestamp/VerificationStateCache.kt @@ -29,15 +29,15 @@ class VerificationStateCache { fun verify( event: OtsEvent, - resolverBuilder: () -> OtsResolver, + resolverBuilder: OtsResolverBuilder, ): VerificationState { cache.put(event.id, VerificationState.Verifying) - return event.verifyState(resolverBuilder()).also { cache.put(event.id, it) } + return event.verifyState(resolverBuilder.build()).also { cache.put(event.id, it) } } fun cacheVerify( event: OtsEvent, - resolverBuilder: () -> OtsResolver, + resolverBuilder: OtsResolverBuilder, ): VerificationState = when (val verif = cache[event.id]) { is VerificationState.Verifying -> verif @@ -45,7 +45,7 @@ class VerificationStateCache { is VerificationState.NetworkError -> { // try again in 5 mins if (verif.time < TimeUtils.fiveMinutesAgo()) { - event.verifyState(resolverBuilder()).also { cache.put(event.id, it) } + event.verifyState(resolverBuilder.build()).also { cache.put(event.id, it) } } else { verify(event, resolverBuilder) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt index 48c937658d..e2768c4590 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapRequestEvent.kt @@ -87,8 +87,8 @@ class LnZapRequestEvent( const val ALT = "Zap request" suspend fun create( - originalNote: Event, - relays: Set, + zappedEvent: Event, + relays: Set, signer: NostrSigner, pollOption: Int?, message: String, @@ -98,13 +98,13 @@ class LnZapRequestEvent( ): LnZapRequestEvent { var tags = listOf( - arrayOf("e", originalNote.id), - arrayOf("p", toUserPubHex ?: originalNote.pubKey), - arrayOf("relays") + relays, + arrayOf("e", zappedEvent.id), + arrayOf("p", toUserPubHex ?: zappedEvent.pubKey), + arrayOf("relays") + relays.map { it.url }, AltTag.assemble(ALT), ) - if (originalNote is AddressableEvent) { - tags = tags + listOf(arrayOf("a", originalNote.aTag().toTag())) + if (zappedEvent is AddressableEvent) { + tags = tags + listOf(arrayOf("a", zappedEvent.aTag().toTag())) } if (pollOption != null && pollOption >= 0) { tags = tags + listOf(arrayOf(PollOptionTag.TAG_NAME, pollOption.toString())) From c7e81d13787b8448a9cce84f11924da5c8e67670 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Jul 2025 10:33:50 -0400 Subject: [PATCH 108/185] Refactoring Channel modules --- .../vitorpamplona/amethyst/model/Account.kt | 2 + .../vitorpamplona/amethyst/model/Channel.kt | 156 +----------------- .../amethyst/model/LocalCache.kt | 5 +- .../model/emphChat/EphemeralChatChannel.kt | 36 ++++ .../model/emphChat/EphemeralChatListState.kt | 1 - .../nip28PublicChats/PublicChatChannel.kt | 101 ++++++++++++ .../nip28PublicChats/PublicChatListState.kt | 1 - .../LiveActivitiesChannel.kt | 73 ++++++++ .../reqCommand/channel/ChannelObservers.kt | 7 +- ...adataAndLiveActivityWatcherSubAssembler.kt | 4 +- .../ChannelMetadataWatcherSubAssembler.kt | 2 +- .../FilterChannelMetadataCreationById.kt | 2 +- .../FilterChannelMetadataUpdatesById.kt | 2 +- .../FilterLiveStreamUpdatesByAddress.kt | 2 +- .../LiveActivityWatcherSubAssembly.kt | 2 +- .../reqCommand/user/UserObservers.kt | 4 +- .../ui/feeds/ChannelFeedContentState.kt | 2 +- .../amethyst/ui/feeds/ChannelFeedState.kt | 2 +- .../ui/navigation/routes/RouteMaker.kt | 6 +- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 4 +- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 6 +- .../ChannelFromUserFilterSubAssembler.kt | 6 +- .../ChannelPublicFilterSubAssembler.kt | 6 +- .../FilterMessagesToEphemeralChat.kt | 2 +- .../FilterMessagesToLiveStream.kt | 2 +- .../FilterMessagesToPublicChat.kt | 2 +- .../FilterMyMessagesToEphemeralChat.kt | 2 +- .../FilterMyMessagesToLiveActivities.kt | 2 +- .../FilterMyMessagesToPublicChat.kt | 2 +- .../publicChannels/ephemChat/ChannelView.kt | 2 +- .../ephemChat/LoadEphemeralChatChannel.kt | 2 +- .../header/EphemeralChatChannelHeader.kt | 2 +- .../ephemChat/header/EphemeralChatTopBar.kt | 2 +- .../header/ShortEphemeralChatChannelHeader.kt | 2 +- .../header/actions/JoinChatButton.kt | 2 +- .../header/actions/LeaveChatButton.kt | 2 +- .../nip28PublicChat/ChannelView.kt | 2 +- .../header/LongPublicChatChannelHeader.kt | 2 +- .../header/PublicChatChannelHeader.kt | 2 +- .../header/PublicChatTopBar.kt | 2 +- .../header/ShortPublicChatChannelHeader.kt | 2 +- .../header/actions/EditChatButton.kt | 2 +- .../header/actions/JoinChatButton.kt | 2 +- .../header/actions/LeaveChatButton.kt | 2 +- .../header/actions/LinkChatButton.kt | 2 +- .../header/actions/OpenChatButton.kt | 2 +- .../header/actions/ShareChatButton.kt | 2 +- .../metadata/ChannelMetadataScreen.kt | 2 +- .../metadata/ChannelMetadataViewModel.kt | 2 +- .../nip53LiveActivities/ChannelView.kt | 2 +- .../LiveActivitiesChannelHeader.kt | 2 +- .../LongLiveActivityChannelHeader.kt | 2 +- .../ShortLiveActivityChannelHeader.kt | 2 +- .../nip53LiveActivities/ShowVideoStreaming.kt | 2 +- .../header/LiveActivityTopBar.kt | 2 +- .../send/ChannelFileUploadDialog.kt | 4 +- .../send/ChannelNewMessageViewModel.kt | 6 +- .../chats/rooms/ChatroomHeaderCompose.kt | 8 +- .../RenderPublicChatChannelThumb.kt | 12 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 2 +- .../home/live/RenderEphemeralBubble.kt | 2 +- .../ui/screen/loggedIn/search/SearchScreen.kt | 7 +- 63 files changed, 305 insertions(+), 234 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 8f233b3b65..e4480525ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListDecryptionCache import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState @@ -41,6 +42,7 @@ import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState import com.vitorpamplona.amethyst.model.nip18Reposts.RepostAction import com.vitorpamplona.amethyst.model.nip25Reactions.ReactionAction +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListDecryptionCache import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListState import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 7e6936fdc5..df0ac0bac8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -22,151 +22,13 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder -import com.vitorpamplona.amethyst.ui.note.toShortDisplay -import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle -import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag -import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address -import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList -import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists -import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress -import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent -import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.LargeCache import kotlinx.coroutines.flow.MutableStateFlow -@Stable -class EphemeralChatChannel( - val roomId: RoomId, -) : Channel() { - override fun relays() = setOf(roomId.relayUrl) - - override fun toBestDisplayName() = roomId.toDisplayKey() - - override fun summary(): String? = null - - override fun profilePicture(): String? = null - - override fun anyNameStartsWith(prefix: String): Boolean = roomId.id.contains(prefix, true) -} - -@Stable -class PublicChatChannel( - val idHex: String, -) : Channel() { - var event: ChannelCreateEvent? = null - var infoTags = EmptyTagList - var info = ChannelDataNorm(null, null, null, null) - - override fun relays() = info.relays?.toSet() ?: super.relays() - - fun relayHintUrls() = relays().take(3) - - fun relayHintUrl() = relays().firstOrNull() - - fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, relayHintUrls()) - - fun toNostrUri() = "nostr:${toNEvent()}" - - fun toEventHint() = event?.let { EventHintBundle(it, relayHintUrl(), null) } - - fun toEventId() = EventIdHintOptional(idHex, relayHintUrl()) - - fun updateChannelInfo( - creator: User, - event: ChannelCreateEvent, - ) { - this.event = event - this.infoTags = event.tags.toImmutableListOfLists() - updateChannelInfo(creator, event.channelInfo(), event.createdAt) - } - - fun updateChannelInfo( - creator: User, - event: ChannelMetadataEvent, - ) { - this.infoTags = event.tags.toImmutableListOfLists() - updateChannelInfo(creator, event.channelInfo(), event.createdAt) - } - - fun updateChannelInfo( - creator: User, - channelInfo: ChannelDataNorm, - updatedAt: Long, - ) { - this.info = channelInfo - super.updateChannelInfo(creator, updatedAt) - } - - override fun toBestDisplayName(): String = info.name ?: toNEvent().toShortDisplay() - - override fun summary(): String? = info.about - - override fun profilePicture(): String? { - if (info.picture.isNullOrBlank()) return super.profilePicture() - return info.picture ?: super.profilePicture() - } - - override fun anyNameStartsWith(prefix: String): Boolean = - idHex.startsWith(prefix) || - info.name?.contains(prefix, true) == true || - info.about?.contains(prefix, true) == true -} - -@Stable -class LiveActivitiesChannel( - val address: Address, -) : Channel() { - var info: LiveActivitiesEvent? = null - - fun address() = address - - override fun relays() = info?.allRelayUrls()?.toSet() ?: super.relays() - - fun relayHintUrl() = relays().firstOrNull() - - fun relayHintUrls() = relays().take(3) - - fun updateChannelInfo( - creator: User, - channelInfo: LiveActivitiesEvent, - updatedAt: Long, - ) { - this.info = channelInfo - super.updateChannelInfo(creator, updatedAt) - } - - override fun toBestDisplayName(): String = info?.title() ?: toNAddr().toShortDisplay() - - override fun summary(): String? = info?.summary() - - override fun profilePicture(): String? = info?.image()?.ifBlank { null } - - override fun anyNameStartsWith(prefix: String): Boolean = - info?.title()?.contains(prefix, true) == true || - info?.summary()?.contains(prefix, true) == true - - fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrls()) - - fun toATag() = ATag(address, relayHintUrl()) - - fun toNostrUri() = "nostr:${toNAddr()}" -} - -data class Counter( - var number: Int = 0, -) - @Stable abstract class Channel { - var creator: User? = null - var updatedMetadataAt: Long = 0 val notes = LargeCache() var lastNote: Note? = null @@ -174,12 +36,6 @@ abstract class Channel { abstract fun toBestDisplayName(): String - open fun summary(): String? = null - - open fun creatorName(): String? = creator?.toBestDisplayName() - - open fun profilePicture(): String? = creator?.info?.banner - open fun relays(): Set = relays.keys .toSortedSet { o1, o2 -> @@ -188,13 +44,7 @@ abstract class Channel { o2Count.compareTo(o1Count) // descending } - open fun updateChannelInfo( - creator: User, - updatedAt: Long, - ) { - this.creator = creator - this.updatedMetadataAt = updatedAt - + fun updateChannelInfo() { flowSet?.metadata?.invalidateData() } @@ -297,6 +147,10 @@ abstract class Channel { } } +data class Counter( + var number: Int = 0, +) + @Stable class ChannelFlowSet( u: Channel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 36bfec4286..2e26088071 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -25,7 +25,10 @@ import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.isDebug +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag import com.vitorpamplona.amethyst.model.privateChats.ChatroomList @@ -843,7 +846,7 @@ object LocalCache : ILocalCache { val creator = event.host()?.let { checkGetOrCreateUser(it.pubKey) } ?: author - channel.updateChannelInfo(creator, event, event.createdAt) + channel.updateChannelInfo(creator, event) refreshNewNoteObservers(note) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt new file mode 100644 index 0000000000..ac9ad74a4c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.emphChat + +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId + +@Stable +class EphemeralChatChannel( + val roomId: RoomId, +) : Channel() { + override fun relays() = setOf(roomId.relayUrl) + + override fun toBestDisplayName() = roomId.toDisplayKey() + + override fun anyNameStartsWith(prefix: String): Boolean = roomId.id.contains(prefix, true) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt index ebc38b8dc2..b8a5368215 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.model.emphChat import android.util.Log import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt new file mode 100644 index 0000000000..0018d3b729 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip28PublicChats + +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.note.toShortDisplay +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional +import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList +import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm + +@Stable +class PublicChatChannel( + val idHex: String, +) : Channel() { + var creator: User? = null + var event: ChannelCreateEvent? = null + + var info = ChannelDataNorm(null, null, null, null) + var infoTags = EmptyTagList + var updatedMetadataAt: Long = 0 + + override fun relays() = info.relays?.toSet() ?: super.relays() + + fun relayHintUrls() = relays().take(3) + + fun relayHintUrl() = relays().firstOrNull() + + fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, relayHintUrls()) + + fun toNostrUri() = "nostr:${toNEvent()}" + + fun toEventHint() = event?.let { EventHintBundle(it, relayHintUrl(), null) } + + fun toEventId() = EventIdHintOptional(idHex, relayHintUrl()) + + fun updateChannelInfo( + creator: User, + event: ChannelCreateEvent, + ) { + this.creator = creator + this.event = event + + this.info = event.channelInfo() + this.infoTags = event.tags.toImmutableListOfLists() + this.updatedMetadataAt = event.createdAt + + updateChannelInfo() + } + + fun updateChannelInfo( + creator: User, + event: ChannelMetadataEvent, + ) { + this.creator = creator + + this.info = event.channelInfo() + this.infoTags = event.tags.toImmutableListOfLists() + this.updatedMetadataAt = event.createdAt + + super.updateChannelInfo() + } + + override fun toBestDisplayName(): String = info.name ?: toNEvent().toShortDisplay() + + fun summary(): String? = info.about + + fun profilePicture(): String? { + if (info.picture.isNullOrBlank()) return creator?.info?.banner + return info.picture ?: creator?.info?.banner + } + + override fun anyNameStartsWith(prefix: String): Boolean = + idHex.startsWith(prefix) || + info.name?.contains(prefix, true) == true || + info.about?.contains(prefix, true) == true +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt index 16218792a0..8ee8452d52 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt @@ -26,7 +26,6 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt new file mode 100644 index 0000000000..cf05a495e1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip53LiveActivities + +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.note.toShortDisplay +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent + +@Stable +class LiveActivitiesChannel( + val address: Address, +) : Channel() { + var creator: User? = null + var info: LiveActivitiesEvent? = null + + fun address() = address + + override fun relays() = info?.allRelayUrls()?.toSet() ?: super.relays() + + fun relayHintUrl() = relays().firstOrNull() + + fun relayHintUrls() = relays().take(3) + + fun updateChannelInfo( + creator: User, + channelInfo: LiveActivitiesEvent, + ) { + this.info = channelInfo + this.creator = creator + super.updateChannelInfo() + } + + override fun toBestDisplayName(): String = info?.title() ?: toNAddr().toShortDisplay() + + fun creatorName(): String? = creator?.toBestDisplayName() + + fun summary(): String? = info?.summary() + + fun profilePicture(): String? = info?.image()?.ifBlank { null } + + override fun anyNameStartsWith(prefix: String): Boolean = + info?.title()?.contains(prefix, true) == true || + info?.summary()?.contains(prefix, true) == true + + fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrls()) + + fun toATag() = ATag(address, relayHintUrl()) + + fun toNostrUri() = "nostr:${toNAddr()}" +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index 72a604220b..7e83e71498 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -26,8 +26,9 @@ import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.ChannelState -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import kotlinx.collections.immutable.ImmutableList @@ -89,7 +90,7 @@ fun observeChannelNoteAuthors( @OptIn(ExperimentalCoroutinesApi::class) @Composable fun observeChannelPicture( - baseChannel: Channel, + baseChannel: PublicChatChannel, accountViewModel: AccountViewModel, ): State { // Subscribe in the relay for changes in the metadata of this user. @@ -101,7 +102,7 @@ fun observeChannelPicture( baseChannel .flow() .metadata.stateFlow - .mapLatest { it.channel.profilePicture() } + .mapLatest { (it.channel as? PublicChatChannel)?.profilePicture() } .distinctUntilChanged() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt index e334422a13..541605b070 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.filterChannelMetadataUpdatesById diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt index 8022e81cb2..f1a15eea27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt index 26d82439ec..8841556149 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt index 067e3c9b2d..86c876a9ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt index 52f87f347c..07f98233bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt index 2a64c9c63b..739c118b60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 2719793619..6b354e41da 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -27,12 +27,12 @@ import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt index ae0cfcf0b2..fa561cbf6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt @@ -24,8 +24,8 @@ import android.util.Log import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.AdditiveComplexFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt index 8bf9e8cd35..1c6fa31587 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import kotlinx.coroutines.flow.MutableStateFlow @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index cd5fbcf93c..706f2503ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -21,12 +21,12 @@ package com.vitorpamplona.amethyst.ui.navigation.routes import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 9a41ff5b1b..b1a1ed18b0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -31,10 +31,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses import com.vitorpamplona.amethyst.ui.components.GenericLoadable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 73f178b671..42fcd2704c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -54,7 +54,7 @@ import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index be94cc1c71..6810476c92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -47,14 +47,14 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier import com.vitorpamplona.amethyst.service.Nip11CachedRetriever diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt index 398515fc9d..df4767a042 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt index 4f6c3bf5df..2330cefaeb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt index fc208afdf0..f6cde11b5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt index fb23067107..78aa37ce20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt index 61c5bcf30e..256c3862a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt index 0727ccda94..84988cf436 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt index c020a9a6ad..eef197437e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt index 681e36aafc..3fded09c0b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index 381d21df8b..c843e8cf16 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt index fd87e59576..b7c943c699 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.note.produceStateIfNotNull import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt index 3b24c1b669..7a529fd864 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt @@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt index 54973442ac..bcc891f4e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt index 9e92d4873f..8a1a851880 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt @@ -38,8 +38,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.FeatureSetType +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt index 1bf4211e18..403881bd07 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt index c6119b2208..ea4ff20c2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index 36a0299877..0d441a6d71 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt index 24a022fe00..625232a29b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -42,7 +42,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt index 05d43fd87e..3279af82d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt index 842d0872f4..2ba8c3ca0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt index e5d265ffa0..e468db4a25 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel import com.vitorpamplona.amethyst.ui.components.LoadNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt index a2d1e9b2ab..37e17b278b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt index 7ee041c53d..230a8b4f5e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt index 54e10431c2..10a6233430 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt index 856c01edb1..abfd1b7ce2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt index d94179393a..1a95f0364b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt index 44a11f3781..57013ff8ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.njumpLink import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index 0df3d24505..868a31b12b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 2995195576..dc2b2d24e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -32,7 +32,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt index e3a32fa12a..3cd0cd09ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt index 7ff1f2be66..7005fcde5d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LiveActivitiesChannelHeader.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt index 04e2be6165..6f4d0918c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/LongLiveActivityChannelHeader.kt @@ -40,9 +40,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt index 4ff5e91941..0517c843cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShortLiveActivityChannelHeader.kt @@ -36,9 +36,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LikeReaction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt index a948aac873..98167d056e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelInfo import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt index d9ae207d4e..fa6e0516e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index b7313cdbd1..ac5d4d9ea6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel -import com.vitorpamplona.amethyst.model.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 5f04be6ed3..0132607254 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -37,13 +37,13 @@ import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState +import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index c3c3cb9868..57f5e6d7ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -54,8 +54,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName @@ -160,8 +160,10 @@ private fun ChannelRoomCompose( val authorName by observeUserName(lastMessage.author!!, accountViewModel) val channelState by observeChannel(channel, accountViewModel) - val channelPicture = channelState?.channel?.profilePicture() ?: channel.profilePicture() - val channelName = channelState?.channel?.toBestDisplayName() ?: channel.toBestDisplayName() + val channel = channelState?.channel as? PublicChatChannel ?: return + + val channelPicture = channel.profilePicture() + val channelName = channel.toBestDisplayName() val noteEvent = lastMessage.event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 44e0a3bd8b..53c5f25792 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -43,8 +43,8 @@ import coil3.compose.AsyncImagePainter import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder -import com.vitorpamplona.amethyst.model.PublicChatChannel import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter @@ -93,13 +93,11 @@ fun RenderPublicChatChannelThumb( nav: INav, ) { val channelUpdates by observeChannel(channel, accountViewModel) + val publicChat = channelUpdates?.channel as PublicChatChannel - val name = remember(channelUpdates) { channelUpdates?.channel?.toBestDisplayName() ?: "" } - val description = remember(channelUpdates) { channelUpdates?.channel?.summary()?.ifBlank { null } } - var cover by - remember(channelUpdates) { - mutableStateOf(channelUpdates?.channel?.profilePicture()?.ifBlank { null }) - } + val name = remember(channelUpdates) { publicChat.toBestDisplayName() } + val description = remember(channelUpdates) { publicChat.summary()?.ifBlank { null } } + var cover by remember(channelUpdates) { mutableStateOf(publicChat.profilePicture()?.ifBlank { null }) } var participantUsers by remember(baseNote) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index a76b2ac972..d15e5a3362 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.EphemeralChatChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt index f722af5088..e56e00ac3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.EphemeralChatChannel +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelNoteAuthors import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index 65f4d52e0d..c2f9ae4bc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.SearchIcon import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.ChannelName import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness @@ -295,9 +296,11 @@ private fun DisplaySearchResults( ephemeralChannels, key = { _, item -> "ephem" + item.roomId.toKey() }, ) { _, item -> + val relayInfo by loadRelayInfo(item.roomId.relayUrl, accountViewModel) + ChannelName( channelIdHex = item.roomId.toKey(), - channelPicture = item.profilePicture(), + channelPicture = relayInfo.icon, channelTitle = { Text( item.toBestDisplayName(), @@ -305,7 +308,7 @@ private fun DisplaySearchResults( ) }, channelLastTime = null, - channelLastContent = item.summary(), + channelLastContent = stringRes(R.string.ephemeral_relay_chat), hasNewMessages = false, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, From 8fe13717dd41af8539ef01b9afb6d5783e7087e9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Jul 2025 10:34:09 -0400 Subject: [PATCH 109/185] Fixes trying to parse empty addresses --- .../quartz/nip01Core/tags/addressables/Address.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt index 358d53559c..3ce8551533 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt @@ -63,8 +63,9 @@ data class Address( ) = "$kind:$pubKeyHex:$dTag" @JvmStatic - fun parse(addressId: String): Address? = - try { + fun parse(addressId: String): Address? { + if (addressId.isBlank()) return null + return try { val parts = addressId.split(":", limit = 3) if (parts.size > 2 && parts[1].length == 64 && Hex.isHex(parts[1])) { Address(parts[0].toInt(), parts[1], parts.getOrNull(2) ?: "") @@ -74,11 +75,11 @@ data class Address( if (addr is NAddress) { addr.address() } else { - Log.w("AddressableId", "Error parsing. Pubkey is not hex: $addressId") + Log.w("AddressableId", "Error parsing. naddr1 seems invalid: $addressId") null } } else { - Log.w("AddressableId", "Error parsing. Pubkey is not hex: $addressId") + Log.w("AddressableId", "Error parsing. Not a valid address: $addressId") null } } @@ -86,6 +87,7 @@ data class Address( Log.e("AddressableId", "Error parsing: $addressId: ${t.message}", t) null } + } fun isOfKind( addressId: String, From e6b5d51cda9435709d6cf23d295118a0b6018317 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Jul 2025 10:34:42 -0400 Subject: [PATCH 110/185] Fixes NIP-55 return as a list of results on the intent. --- .../quartz/nip01Core/jackson/JsonMapper.kt | 7 +- .../api/foreground/IntentRequestManager.kt | 72 +++++++++++-------- .../intents/responses/DecryptZapResponse.kt | 5 +- .../intents/responses/DeriveKeyResponse.kt | 6 +- .../intents/responses/LoginResponse.kt | 8 +-- .../intents/responses/Nip04DecryptResponse.kt | 6 +- .../intents/responses/Nip04EncryptResponse.kt | 6 +- .../intents/responses/Nip44DecryptResponse.kt | 6 +- .../intents/responses/Nip44EncryptResponse.kt | 6 +- .../intents/responses/SignResponse.kt | 8 +-- .../intents/results/IntentResult.kt | 57 +++++++++++++++ .../results/IntentResultJsonDeserializer.kt | 41 +++++++++++ .../results/IntentResultJsonSerializer.kt | 40 +++++++++++ .../client/ExternalSignerLogin.kt | 6 +- 14 files changed, 216 insertions(+), 58 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonDeserializer.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonSerializer.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt index b365d765dd..3a24f550fc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt @@ -35,6 +35,9 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Request import com.vitorpamplona.quartz.nip47WalletConnect.RequestDeserializer import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip47WalletConnect.ResponseDeserializer +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResultJsonDeserializer +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResultJsonSerializer import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.PermissionDeserializer import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.PermissionSerializer @@ -65,7 +68,9 @@ class JsonMapper { .addSerializer(BunkerResponse::class.java, BunkerResponse.BunkerResponseSerializer()) .addDeserializer(BunkerResponse::class.java, BunkerResponse.BunkerResponseDeserializer()) .addDeserializer(Permission::class.java, PermissionDeserializer()) - .addSerializer(Permission::class.java, PermissionSerializer()), + .addSerializer(Permission::class.java, PermissionSerializer()) + .addDeserializer(IntentResult::class.java, IntentResultJsonDeserializer()) + .addSerializer(IntentResult::class.java, IntentResultJsonSerializer()), ) fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt index d10768450b..8fc522961b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/IntentRequestManager.kt @@ -23,13 +23,13 @@ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground import android.content.ActivityNotFoundException import android.content.Intent import android.util.Log -import android.util.Log.e import androidx.collection.LruCache import com.vitorpamplona.quartz.nip55AndroidSigner.api.IResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult import com.vitorpamplona.quartz.utils.RandomInstance import com.vitorpamplona.quartz.utils.tryAndWait -import kotlinx.coroutines.CancellationException +import kotlin.collections.forEach import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -61,7 +61,7 @@ class IntentRequestManager( val activityNotFoundIntent = Intent() // LRU cache to store pending requests and their continuations. - private val awaitingRequests = LruCache>(2000) + private val awaitingRequests = LruCache>(2000) // Function to launch an Intent in the foreground. private var appLauncher: ((Intent) -> Unit)? = null @@ -79,10 +79,21 @@ class IntentRequestManager( } fun newResponse(data: Intent) { - val callId = data.getStringExtra("id") - if (callId != null) { - awaitingRequests[callId]?.resume(data) - awaitingRequests.remove(callId) + val results = data.getStringExtra("results") + if (results != null) { + // This happens when the intent responds to many requests at the same time. + IntentResult.fromJsonArray(results).forEach { result -> + if (result.id != null) { + awaitingRequests[result.id]?.resume(result) + awaitingRequests.remove(result.id) + } + } + } else { + val result = IntentResult.fromIntent(data) + if (result.id != null) { + awaitingRequests[result.id]?.resume(result) + awaitingRequests.remove(result.id) + } } } @@ -102,7 +113,7 @@ class IntentRequestManager( */ suspend fun launchWaitAndParse( requestIntentBuilder: () -> Intent, - parser: (intent: Intent) -> SignerResult.RequestAddressed, + parser: (intent: IntentResult) -> SignerResult.RequestAddressed, ): SignerResult.RequestAddressed = appLauncher?.let { launcher -> val requestIntent = requestIntentBuilder() @@ -111,32 +122,31 @@ class IntentRequestManager( requestIntent.putExtra("id", callId) requestIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) - val resultIntent = - tryAndWait(foregroundApprovalTimeout) { continuation -> - continuation.invokeOnCancellation { - awaitingRequests.remove(callId) + try { + val resultIntent = + tryAndWait(foregroundApprovalTimeout) { continuation -> + continuation.invokeOnCancellation { + awaitingRequests.remove(callId) + } + + awaitingRequests.put(callId, continuation) + + try { + launcher.invoke(requestIntent) + } catch (e: Exception) { + Log.e("ExternalSigner", "Error launching intent", e) + awaitingRequests.remove(callId) + throw e + } } - awaitingRequests.put(callId, continuation) - - try { - launcher.invoke(requestIntent) - } catch (e: ActivityNotFoundException) { - Log.e("ExternalSigner", "Error launching intent", e) - awaitingRequests.remove(callId) - continuation.resume(activityNotFoundIntent) - } catch (e: Exception) { - Log.e("ExternalSigner", "Error launching intent", e) - awaitingRequests.remove(callId) - continuation.resume(null) - if (e is CancellationException) throw e - } + when (resultIntent) { + null -> SignerResult.RequestAddressed.TimedOut() + else -> parser(resultIntent) } - - when (resultIntent) { - null -> SignerResult.RequestAddressed.TimedOut() - activityNotFoundIntent -> SignerResult.RequestAddressed.SignerNotFound() - else -> parser(resultIntent) + } catch (e: ActivityNotFoundException) { + Log.e("ExternalSigner", "Error launching intent: Signer not found", e) + SignerResult.RequestAddressed.SignerNotFound() } } ?: SignerResult.RequestAddressed.NoActivityToLaunchFrom() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt index 0d602809bf..baac547770 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt @@ -24,6 +24,7 @@ import android.content.Intent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent class DecryptZapResponse { @@ -34,8 +35,8 @@ class DecryptZapResponse { return intent } - fun parse(intent: Intent): SignerResult.RequestAddressed { - val eventJson = intent.getStringExtra("result") + fun parse(intent: IntentResult): SignerResult.RequestAddressed { + val eventJson = intent.result return if (!eventJson.isNullOrBlank()) { if (eventJson.startsWith("{")) { val event = Event.fromJsonOrNull(eventJson) as? LnZapPrivateEvent diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt index 8a4a3085ce..6369b596e6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt @@ -20,14 +20,14 @@ */ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses -import android.content.Intent import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult class DeriveKeyResponse { companion object { - fun parse(intent: Intent): SignerResult.RequestAddressed { - val newPrivateKey = intent.getStringExtra("result") + fun parse(intent: IntentResult): SignerResult.RequestAddressed { + val newPrivateKey = intent.result return if (newPrivateKey != null) { SignerResult.RequestAddressed.Successful(DerivationResult(newPrivateKey)) } else { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt index 1a5cca5560..5594a5156a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt @@ -20,15 +20,15 @@ */ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses -import android.content.Intent import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult class LoginResponse { companion object { - fun parse(intent: Intent): SignerResult.RequestAddressed { - val pubkey = intent.getStringExtra("result") - val packageName = intent.getStringExtra("package") + fun parse(intent: IntentResult): SignerResult.RequestAddressed { + val pubkey = intent.result + val packageName = intent.`package` return if (pubkey != null && packageName != null) { SignerResult.RequestAddressed.Successful( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt index c7e78b366b..2b9d0cc4c1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt @@ -20,14 +20,14 @@ */ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses -import android.content.Intent import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult class Nip04DecryptResponse { companion object { - fun parse(intent: Intent): SignerResult.RequestAddressed { - val ciphertext = intent.getStringExtra("result") + fun parse(intent: IntentResult): SignerResult.RequestAddressed { + val ciphertext = intent.result return if (ciphertext != null) { SignerResult.RequestAddressed.Successful(DecryptionResult(ciphertext)) } else { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt index d16ea4d7b7..35b58ff645 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt @@ -20,14 +20,14 @@ */ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses -import android.content.Intent import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult class Nip04EncryptResponse { companion object { - fun parse(intent: Intent): SignerResult.RequestAddressed { - val ciphertext = intent.getStringExtra("result") + fun parse(intent: IntentResult): SignerResult.RequestAddressed { + val ciphertext = intent.result return if (ciphertext != null) { SignerResult.RequestAddressed.Successful(EncryptionResult(ciphertext)) } else { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt index f320afb79b..fc3a78b899 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt @@ -20,14 +20,14 @@ */ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses -import android.content.Intent import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult class Nip44DecryptResponse { companion object { - fun parse(intent: Intent): SignerResult.RequestAddressed { - val ciphertext = intent.getStringExtra("result") + fun parse(intent: IntentResult): SignerResult.RequestAddressed { + val ciphertext = intent.result return if (ciphertext != null) { SignerResult.RequestAddressed.Successful(DecryptionResult(ciphertext)) } else { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt index c9a9412bda..b8cd344660 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt @@ -20,14 +20,14 @@ */ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses -import android.content.Intent import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult class Nip44EncryptResponse { companion object { - fun parse(intent: Intent): SignerResult.RequestAddressed { - val ciphertext = intent.getStringExtra("result") + fun parse(intent: IntentResult): SignerResult.RequestAddressed { + val ciphertext = intent.result return if (ciphertext != null) { SignerResult.RequestAddressed.Successful(EncryptionResult(ciphertext)) } else { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt index 25d040ca46..e6cc27b8ff 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt @@ -20,20 +20,20 @@ */ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses -import android.content.Intent import com.vitorpamplona.quartz.EventFactory import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult class SignResponse { companion object { fun parse( - intent: Intent, + intent: IntentResult, unsignedEvent: Event, ): SignerResult.RequestAddressed { - val eventJson = intent.getStringExtra("event") + val eventJson = intent.event return if (eventJson != null) { if (eventJson.startsWith("{")) { val event = Event.fromJsonOrNull(eventJson) @@ -50,7 +50,7 @@ class SignResponse { SignerResult.RequestAddressed.ReceivedButCouldNotPerform(eventJson) } } else { - val signature = intent.getStringExtra("result") + val signature = intent.result if (signature != null && signature.length == 128) { val event: Event = EventFactory.create( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt new file mode 100644 index 0000000000..7bd374ac33 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results + +import android.content.Intent +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper + +class IntentResult( + val `package`: String?, + val result: String?, + val event: String?, + val id: String?, +) { + fun toJson(): String = JsonMapper.mapper.writeValueAsString(this) + + fun toIntent(): Intent { + val intent = Intent() + intent.putExtra("id", id) + intent.putExtra("result", result) + intent.putExtra("event", event) + intent.putExtra("package", `package`) + return intent + } + + companion object { + fun fromIntent(data: Intent): IntentResult = + IntentResult( + id = data.getStringExtra("id"), + result = data.getStringExtra("result"), + event = data.getStringExtra("event"), + `package` = data.getStringExtra("package"), + ) + + fun fromJson(json: String): IntentResult = JsonMapper.mapper.readValue(json) + + fun fromJsonArray(json: String): List = JsonMapper.mapper.readValue>(json) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonDeserializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonDeserializer.kt new file mode 100644 index 0000000000..e53cb8be22 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonDeserializer.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.deser.std.StdDeserializer + +class IntentResultJsonDeserializer : StdDeserializer(IntentResult::class.java) { + override fun deserialize( + jp: JsonParser, + ctxt: DeserializationContext, + ): IntentResult { + val jsonObject: JsonNode = jp.codec.readTree(jp) + return IntentResult( + `package` = jsonObject.get("package")?.asText()?.intern(), + result = jsonObject.get("result")?.asText()?.intern(), + event = jsonObject.get("event")?.asText()?.intern(), + id = jsonObject.get("id")?.asText()?.intern(), + ) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonSerializer.kt new file mode 100644 index 0000000000..289f4daed0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResultJsonSerializer.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer + +class IntentResultJsonSerializer : StdSerializer(IntentResult::class.java) { + override fun serialize( + result: IntentResult, + gen: JsonGenerator, + provider: SerializerProvider, + ) { + gen.writeStartObject() + result.`package`?.let { gen.writeStringField("package", it) } + result.result?.let { gen.writeStringField("result", it) } + result.event?.let { gen.writeStringField("event", it) } + result.id?.let { gen.writeStringField("id", it) } + gen.writeEndObject() + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt index b5e3fb0926..015c3fc65f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.LoginRequest import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.LoginResponse +import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission object ExternalSignerLogin { @@ -34,5 +35,8 @@ object ExternalSignerLogin { return intent } - fun parseResult(data: Intent): SignerResult = LoginResponse.parse(data) + fun parseResult(data: Intent): SignerResult { + val result = IntentResult.fromIntent(data) + return LoginResponse.parse(result) + } } From a3ec4d42604fff3e1462384fe23d79f74e4ec60b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Jul 2025 10:54:24 -0400 Subject: [PATCH 111/185] Completing Intent writing implementation --- .../intents/responses/DecryptZapResponse.kt | 10 ++++------ .../foreground/intents/responses/DeriveKeyResponse.kt | 6 ++++++ .../api/foreground/intents/responses/LoginResponse.kt | 10 ++++++++++ .../intents/responses/Nip04DecryptResponse.kt | 11 ++++++++--- .../intents/responses/Nip04EncryptResponse.kt | 5 +++++ .../intents/responses/Nip44DecryptResponse.kt | 11 ++++++++--- .../intents/responses/Nip44EncryptResponse.kt | 5 +++++ .../api/foreground/intents/responses/SignResponse.kt | 6 ++++++ .../api/foreground/intents/results/IntentResult.kt | 10 +++++----- 9 files changed, 57 insertions(+), 17 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt index baac547770..35b3474a2c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DecryptZapResponse.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses -import android.content.Intent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult @@ -29,11 +28,10 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent class DecryptZapResponse { companion object { - fun assemble(event: LnZapPrivateEvent): Intent { - val intent = Intent() - intent.putExtra("result", event.toJson()) - return intent - } + fun assemble(event: LnZapPrivateEvent): IntentResult = + IntentResult( + result = event.toJson(), + ) fun parse(intent: IntentResult): SignerResult.RequestAddressed { val eventJson = intent.result diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt index 6369b596e6..3d51fb9a5c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/DeriveKeyResponse.kt @@ -20,12 +20,18 @@ */ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult class DeriveKeyResponse { companion object { + fun assemble(newPrivateKey: HexKey): IntentResult = + IntentResult( + result = newPrivateKey, + ) + fun parse(intent: IntentResult): SignerResult.RequestAddressed { val newPrivateKey = intent.result return if (newPrivateKey != null) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt index 5594a5156a..ce52787213 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/LoginResponse.kt @@ -20,12 +20,22 @@ */ package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult class LoginResponse { companion object { + fun assemble( + pubkey: HexKey, + packageName: String, + ): IntentResult = + IntentResult( + result = pubkey, + `package` = packageName, + ) + fun parse(intent: IntentResult): SignerResult.RequestAddressed { val pubkey = intent.result val packageName = intent.`package` diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt index 2b9d0cc4c1..84cc20ab66 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04DecryptResponse.kt @@ -26,10 +26,15 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.result class Nip04DecryptResponse { companion object { + fun assemble(plaintext: String): IntentResult = + IntentResult( + result = plaintext, + ) + fun parse(intent: IntentResult): SignerResult.RequestAddressed { - val ciphertext = intent.result - return if (ciphertext != null) { - SignerResult.RequestAddressed.Successful(DecryptionResult(ciphertext)) + val plaintext = intent.result + return if (plaintext != null) { + SignerResult.RequestAddressed.Successful(DecryptionResult(plaintext)) } else { SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt index 35b58ff645..969520fef0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip04EncryptResponse.kt @@ -26,6 +26,11 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.result class Nip04EncryptResponse { companion object { + fun assemble(ciphertext: String): IntentResult = + IntentResult( + result = ciphertext, + ) + fun parse(intent: IntentResult): SignerResult.RequestAddressed { val ciphertext = intent.result return if (ciphertext != null) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt index fc3a78b899..e65dd8f4f3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44DecryptResponse.kt @@ -26,10 +26,15 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.result class Nip44DecryptResponse { companion object { + fun assemble(plaintext: String): IntentResult = + IntentResult( + result = plaintext, + ) + fun parse(intent: IntentResult): SignerResult.RequestAddressed { - val ciphertext = intent.result - return if (ciphertext != null) { - SignerResult.RequestAddressed.Successful(DecryptionResult(ciphertext)) + val plaintext = intent.result + return if (plaintext != null) { + SignerResult.RequestAddressed.Successful(DecryptionResult(plaintext)) } else { SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt index b8cd344660..243856a389 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/Nip44EncryptResponse.kt @@ -26,6 +26,11 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.result class Nip44EncryptResponse { companion object { + fun assemble(ciphertext: String): IntentResult = + IntentResult( + result = ciphertext, + ) + fun parse(intent: IntentResult): SignerResult.RequestAddressed { val ciphertext = intent.result return if (ciphertext != null) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt index e6cc27b8ff..6e8a6d639e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/responses/SignResponse.kt @@ -29,6 +29,12 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.result class SignResponse { companion object { + fun assemble(event: Event): IntentResult = + IntentResult( + event = event.toJson(), + result = event.sig, + ) + fun parse( intent: IntentResult, unsignedEvent: Event, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt index 7bd374ac33..b1beca7a87 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/api/foreground/intents/results/IntentResult.kt @@ -24,11 +24,11 @@ import android.content.Intent import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper -class IntentResult( - val `package`: String?, - val result: String?, - val event: String?, - val id: String?, +data class IntentResult( + val `package`: String? = null, + val result: String? = null, + val event: String? = null, + val id: String? = null, ) { fun toJson(): String = JsonMapper.mapper.writeValueAsString(this) From 0f13202f847e8ab6996e42409fe37c6fc9706ba0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Jul 2025 12:46:05 -0400 Subject: [PATCH 112/185] Corrects a bug of not showing the user's own messages on chat --- .../com/vitorpamplona/amethyst/model/Account.kt | 2 -- .../amethyst/model/privateChats/Chatroom.kt | 2 -- .../amethyst/ui/screen/FeedViewModel.kt | 3 ++- .../ui/screen/loggedIn/AccountViewModel.kt | 17 +++++++++++++++++ .../header/NewChatroomSubjectDialog.kt | 4 +--- .../privateDM/send/ChatNewMessageViewModel.kt | 7 ------- .../chats/privateDM/send/NewGroupDMScreen.kt | 5 ++--- .../send/PrivateMessageEditFieldRow.kt | 5 ++++- amethyst/src/main/res/values/strings.xml | 3 +++ .../quartz/nip17Dm/base/BaseDMGroupEvent.kt | 2 +- 10 files changed, 30 insertions(+), 20 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index e4480525ce..32c66aab6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1424,8 +1424,6 @@ class Account( template: EventTemplate, draftTag: String? = null, ) { - if (!isWriteable()) return - if (draftTag != null) { if (template.content.isEmpty()) { deleteDraft(draftTag) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index 07e0b77bfe..d956b21dce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -42,8 +42,6 @@ class Chatroom { @Synchronized fun addMessageSync(msg: Note) { - checkNotInMainThread() - if (msg !in roomMessages) { roomMessages = roomMessages + msg diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 8df48eb4f8..094f9486c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -53,8 +53,9 @@ abstract class FeedViewModel( init { Log.d("Init", "Starting new Model: ${this.javaClass.simpleName}") collectorJob = - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.Default) { LocalCache.live.newEventBundles.collect { newNotes -> + Log.d("Rendering Metrics", "Update feeds: ${this@FeedViewModel.javaClass.simpleName} with ${newNotes.size}") feedState.updateFeedWith(newNotes) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 6810476c92..ba4798ba20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -765,6 +765,23 @@ class AccountViewModel( R.string.unauthorized_exception, R.string.unauthorized_exception_description, ) + } catch (e: SignerExceptions.SignerNotFoundException) { + toastManager.toast( + R.string.signer_not_found_exception, + R.string.signer_not_found_exception_description, + ) + } catch (e: SignerExceptions.TimedOutException) { + Log.w("AccountViewModel", "TimedOutException", e) + } catch (e: SignerExceptions.NothingToDecrypt) { + Log.w("AccountViewModel", "NothingToDecrypt", e) + } catch (e: SignerExceptions.CouldNotPerformException) { + Log.w("AccountViewModel", "CouldNotPerformException", e) + } catch (e: SignerExceptions.ManuallyUnauthorizedException) { + Log.w("AccountViewModel", "ManuallyUnauthorizedException", e) + } catch (e: SignerExceptions.AutomaticallyUnauthorizedException) { + Log.w("AccountViewModel", "AutomaticallyUnauthorizedException", e) + } catch (e: SignerExceptions.RunningOnBackgroundWithoutAutomaticPermissionException) { + Log.w("AccountViewModel", "TimedOutRunningOnBackgroundWithoutAutomaticPermissionExceptionException", e) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt index ec79a82c65..6f59c7f164 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt @@ -56,8 +56,6 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip17Dm.messages.changeSubject -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun NewChatroomSubjectDialog( @@ -100,7 +98,7 @@ fun NewChatroomSubjectDialog( PostButton( onPost = { - scope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { val template = ChatMessageEvent.build( message.value, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index f2503744d4..6d713786ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -345,13 +345,6 @@ class ChatNewMessageViewModel : nip17 = draftEvent is NIP17Group } - fun sendPost(onDone: () -> Unit) { - viewModelScope.launch(Dispatchers.IO) { - sendPostSync() - onDone() - } - } - suspend fun sendPostSync() { innerSendPost(null) accountViewModel.deleteDraft(draftTag.current) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index c47094f166..0b079e56d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -70,7 +70,6 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R @@ -174,7 +173,7 @@ fun NewGroupDMScreen( onCancel = { // uses the accountViewModel scope to avoid cancelling this // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { postViewModel.sendDraftSync() delay(100) nav.popBack() @@ -184,7 +183,7 @@ fun NewGroupDMScreen( onPost = { // uses the accountViewModel scope to avoid cancelling this // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { postViewModel.sendPostSync() postViewModel.room?.let { nav.nav(routeToMessage(it, null, null, null, accountViewModel)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 0d489578b5..cc557c2d06 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -145,7 +145,10 @@ fun EditField( isActive = channelScreenModel.canPost(), modifier = EditFieldTrailingIconModifier, ) { - channelScreenModel.sendPost(onSendNewMessage) + accountViewModel.runIOCatching { + channelScreenModel.sendPostSync() + onSendNewMessage() + } } }, leadingIcon = { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 141ca6b28c..a9cdda2ec5 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -63,6 +63,9 @@ Unauthorized Decryption The signer did not authorize a decryption that is required to make this operation. Activate NIP-44 decryptions in your signer app and try again + Signer not found + Was the Signer app uninstalled? Check if the signer is installed and has this account. Log off and Log in again of the signer app has changed. + Zaps View count diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt index 3b1ef9ecba..b47e875ceb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/BaseDMGroupEvent.kt @@ -69,7 +69,7 @@ open class BaseDMGroupEvent( return result } - override fun isIncluded(pubKey: HexKey) = tags.any(PTag::isTagged, pubKey) + override fun isIncluded(pubKey: HexKey) = pubKey == this.pubKey || tags.any(PTag::isTagged, pubKey) override fun groupMembers() = recipientsPubKey().plus(pubKey).toSet() From 37ff0070ee1f9740010f8ace981ee910057f9f16 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Jul 2025 13:38:30 -0400 Subject: [PATCH 113/185] Solves the hanging AccountViewModel scope when Amethyst is forced closed. --- .../amethyst/ui/screen/FeedViewModel.kt | 15 ++--- .../amethyst/ui/screen/UserFeedViewModel.kt | 18 ++--- .../ui/screen/loggedIn/AccountViewModel.kt | 65 ++++++++----------- .../profile/zaps/LnZapFeedViewModel.kt | 17 ++--- .../loggedIn/settings/StringFeedViewModel.kt | 18 ++--- 5 files changed, 48 insertions(+), 85 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 094f9486c4..d71826742f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -30,7 +30,6 @@ import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.launch @Stable @@ -48,22 +47,18 @@ abstract class FeedViewModel( override fun invalidateData(ignoreIfDoing: Boolean) = feedState.invalidateData(ignoreIfDoing) - private var collectorJob: Job? = null - init { Log.d("Init", "Starting new Model: ${this.javaClass.simpleName}") - collectorJob = - viewModelScope.launch(Dispatchers.Default) { - LocalCache.live.newEventBundles.collect { newNotes -> - Log.d("Rendering Metrics", "Update feeds: ${this@FeedViewModel.javaClass.simpleName} with ${newNotes.size}") - feedState.updateFeedWith(newNotes) - } + viewModelScope.launch(Dispatchers.Default) { + LocalCache.live.newEventBundles.collect { newNotes -> + Log.d("Rendering Metrics", "Update feeds: ${this@FeedViewModel.javaClass.simpleName} with ${newNotes.size}") + feedState.updateFeedWith(newNotes) } + } } override fun onCleared() { Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") - collectorJob?.cancel() super.onCleared() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 8ee6a446c1..45e7da9b5b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -36,7 +36,6 @@ import com.vitorpamplona.ammolite.relays.BundledUpdate import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -99,26 +98,19 @@ open class UserFeedViewModel( } } - var collectorJob: Job? = null - init { Log.d("Init", "${this.javaClass.simpleName}") - collectorJob = - viewModelScope.launch(Dispatchers.IO) { - checkNotInMainThread() - - LocalCache.live.newEventBundles.collect { newNotes -> - checkNotInMainThread() - - invalidateData() - } + viewModelScope.launch(Dispatchers.Default) { + LocalCache.live.newEventBundles.collect { newNotes -> + Log.d("Rendering Metrics", "Update feeds: ${this@UserFeedViewModel.javaClass.simpleName} with ${newNotes.size}") + invalidateData() } + } } override fun onCleared() { Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") bundler.cancel() - collectorJob?.cancel() super.onCleared() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index ba4798ba20..1b07a14962 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -82,7 +82,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CardFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.CombinedZap import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.tor.TorSettings -import com.vitorpamplona.ammolite.relays.BundledInsert import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryReadingStateEvent @@ -135,7 +134,6 @@ import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -1224,55 +1222,46 @@ class AccountViewModel( override fun create(modelClass: Class): T = AccountViewModel(accountSettings, settings, app) as T } - private var collectorJobNew: Job? = null - private var collectorJobDeleted: Job? = null - private val bundlerInsert = BundledInsert>(3000, Dispatchers.Default) - init { Log.d("Init", "AccountViewModel") - collectorJobNew = - viewModelScope.launch(Dispatchers.Default) { - feedStates.init() - // awaits for init to finish before starting to capture new events. - LocalCache.live.newEventBundles.collect { newNotes -> - if (isDebug) { - Log.d( - "Rendering Metrics", - "Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()} with ${newNotes.size} new notes", - ) - } - logTime("AccountViewModel newEventBundle Update with ${newNotes.size} new notes") { - feedStates.updateFeedsWith(newNotes) - upgradeAttestations() - viewModelScope.launch(Dispatchers.Default) { - newNotesPreProcessor.runNew(newNotes) - } + viewModelScope.launch(Dispatchers.Default) { + feedStates.init() + // awaits for init to finish before starting to capture new events. + LocalCache.live.newEventBundles.collect { newNotes -> + if (isDebug) { + Log.d( + "Rendering Metrics", + "Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()} with ${newNotes.size} new notes", + ) + } + logTime("AccountViewModel newEventBundle Update with ${newNotes.size} new notes") { + feedStates.updateFeedsWith(newNotes) + upgradeAttestations() + viewModelScope.launch(Dispatchers.Default) { + newNotesPreProcessor.runNew(newNotes) } } } + } - collectorJobDeleted = - viewModelScope.launch(Dispatchers.Default) { - LocalCache.live.deletedEventBundles.collect { newNotes -> - if (isDebug) { - Log.d( - "Rendering Metrics", - "Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()} with ${newNotes.size} new notes", - ) - } - logTime("AccountViewModel deletedEventBundle Update with ${newNotes.size} new notes") { - newNotesPreProcessor.runDeleted(newNotes) - } + viewModelScope.launch(Dispatchers.Default) { + LocalCache.live.deletedEventBundles.collect { newNotes -> + if (isDebug) { + Log.d( + "Rendering Metrics", + "Update feeds ${this@AccountViewModel} for ${account.userProfile().toBestDisplayName()} with ${newNotes.size} new notes", + ) + } + logTime("AccountViewModel deletedEventBundle Update with ${newNotes.size} new notes") { + newNotesPreProcessor.runDeleted(newNotes) } } + } } override fun onCleared() { Log.d("Init", "AccountViewModel onCleared") feedStates.destroy() - bundlerInsert.cancel() - collectorJobNew?.cancel() - collectorJobDeleted?.cancel() super.onCleared() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt index 39a4943ab8..77b8e7e560 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.ammolite.relays.BundledUpdate import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -85,26 +84,22 @@ open class LnZapFeedViewModel( } } - var collectorJob: Job? = null - init { Log.d("Init", "${this.javaClass.simpleName}") - collectorJob = - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.IO) { + checkNotInMainThread() + + LocalCache.live.newEventBundles.collect { newNotes -> checkNotInMainThread() - LocalCache.live.newEventBundles.collect { newNotes -> - checkNotInMainThread() - - invalidateData() - } + invalidateData() } + } } override fun onCleared() { Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") bundler.cancel() - collectorJob?.cancel() super.onCleared() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt index 7f1abdfa5a..a98bd98ef7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt @@ -35,7 +35,6 @@ import com.vitorpamplona.ammolite.relays.BundledUpdate import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch @@ -98,26 +97,19 @@ open class StringFeedViewModel( } } - var collectorJob: Job? = null - init { Log.d("Init", this.javaClass.simpleName) - collectorJob = - viewModelScope.launch(Dispatchers.IO) { - checkNotInMainThread() - - LocalCache.live.newEventBundles.collect { newNotes -> - checkNotInMainThread() - - invalidateData() - } + viewModelScope.launch(Dispatchers.Default) { + LocalCache.live.newEventBundles.collect { newNotes -> + Log.d("Rendering Metrics", "Update feeds: ${this@StringFeedViewModel.javaClass.simpleName} with ${newNotes.size}") + invalidateData() } + } } override fun onCleared() { Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") bundler.cancel() - collectorJob?.cancel() super.onCleared() } } From 6c77bf94a8d761cf49ecdb5b894fa47379ce0952 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Jul 2025 14:09:47 -0400 Subject: [PATCH 114/185] Fixing mark as read --- .../amethyst/ui/feeds/FeedContentState.kt | 9 +++++++ .../ui/screen/loggedIn/AccountViewModel.kt | 24 ++++++------------- .../chats/rooms/feed/ChatroomListFeedView.kt | 15 ++---------- .../chats/rooms/feed/ChatroomListTabs.kt | 3 --- .../rooms/singlepane/MessagesSinglePane.kt | 14 ++++------- .../chats/rooms/twopane/ChatroomListPane.kt | 14 ++++------- 6 files changed, 28 insertions(+), 51 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt index 4602d59749..e6b9dccc05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt @@ -72,6 +72,15 @@ class FeedContentState( viewModelScope.launch(Dispatchers.Default) { refreshSuspended() } } + fun visibleNotes(): List { + val currentState = _feedContent.value + return if (currentState is FeedState.Loaded) { + currentState.feed.value.list + } else { + emptyList() + } + } + fun refreshSuspended() { checkNotInMainThread() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 1b07a14962..664fd88150 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -71,7 +71,6 @@ import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.navigation.routes.Route -import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.ZapAmountCommentNotification import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus import com.vitorpamplona.amethyst.ui.note.showAmount @@ -109,6 +108,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay import com.vitorpamplona.quartz.nip19Bech32.entities.NSec +import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect import com.vitorpamplona.quartz.nip47WalletConnect.Response @@ -1188,26 +1188,16 @@ class AccountViewModel( return onIsNew } - fun markAllAsRead( - notes: ImmutableList, - accountViewModel: AccountViewModel, - onDone: () -> Unit, - ) { + fun markAllChatNotesAsRead(notes: List) { viewModelScope.launch(Dispatchers.IO) { for (note in notes) { - note.event?.createdAt?.let { date -> - val route = routeFor(note, accountViewModel.account) - route?.let { - if (route is Route.Room) { - account.markAsRead("Room/${route.id}", date) - } else if (route is Route.PublicChatChannel) { - account.markAsRead("Channel/${route.id}", date) - } - } + val noteEvent = note.event + if (noteEvent is IsInPublicChatChannel) { + account.markAsRead("Channel/${noteEvent.channelId()}", noteEvent.createdAt) + } else if (noteEvent is ChatroomKeyable) { + account.markAsRead("Room/${noteEvent.chatroomKey(account.signer.pubKey).hashCode()}", noteEvent.createdAt) } } - - onDone() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt index 2901cd5054..a43e09e9c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListFeedView.kt @@ -28,8 +28,6 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -53,11 +51,10 @@ fun ChatroomListFeedView( scrollStateKey: String, accountViewModel: AccountViewModel, nav: INav, - markAsRead: MutableState, ) { RefresheableBox(feedContentState, true) { SaveableFeedContentState(feedContentState, scrollStateKey) { listState -> - CrossFadeState(feedContentState, listState, accountViewModel, nav, markAsRead) + CrossFadeState(feedContentState, listState, accountViewModel, nav) } } } @@ -68,7 +65,6 @@ private fun CrossFadeState( listState: LazyListState, accountViewModel: AccountViewModel, nav: INav, - markAsRead: MutableState, ) { val feedState by feedContentState.feedContent.collectAsStateWithLifecycle() @@ -85,7 +81,7 @@ private fun CrossFadeState( FeedError(state.errorMessage) { feedContentState.invalidateData() } } is FeedState.Loaded -> { - FeedLoaded(state, listState, accountViewModel, nav, markAsRead) + FeedLoaded(state, listState, accountViewModel, nav) } FeedState.Loading -> { LoadingFeed() @@ -100,16 +96,9 @@ private fun FeedLoaded( listState: LazyListState, accountViewModel: AccountViewModel, nav: INav, - markAsRead: MutableState, ) { val items by loaded.feed.collectAsStateWithLifecycle() - LaunchedEffect(key1 = markAsRead.value) { - if (markAsRead.value) { - accountViewModel.markAllAsRead(items.list, accountViewModel) { markAsRead.value = false } - } - } - LazyColumn( contentPadding = FeedPadding, state = listState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index 95a77e328b..15a8a63713 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -38,7 +38,6 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -62,7 +61,6 @@ class MessagesTabItem( val resource: Int, val scrollStateKey: String, val feedContentState: FeedContentState, - val markAsRead: MutableState, ) @Composable @@ -132,7 +130,6 @@ fun MessagesPager( scrollStateKey = tabs[page].scrollStateKey, accountViewModel = accountViewModel, nav = nav, - markAsRead = tabs[page].markAsRead, ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt index 28d5be2c30..6503d9f858 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/singlepane/MessagesSinglePane.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.feeds.FeedContentState @@ -53,20 +52,17 @@ fun MessagesSinglePane( ) { val pagerState = rememberPagerState { 2 } - val markKnownAsRead = remember { mutableStateOf(false) } - val markNewAsRead = remember { mutableStateOf(false) } - WatchLifecycleAndUpdateModel(knownFeedContentState) WatchLifecycleAndUpdateModel(newFeedContentState) ChatroomListFilterAssemblerSubscription(accountViewModel) val tabs by - remember(knownFeedContentState, markKnownAsRead) { + remember(knownFeedContentState) { derivedStateOf { listOf( - MessagesTabItem(R.string.known, ScrollStateKeys.MESSAGES_KNOWN, knownFeedContentState, markKnownAsRead), - MessagesTabItem(R.string.new_requests, ScrollStateKeys.MESSAGES_NEW, newFeedContentState, markNewAsRead), + MessagesTabItem(R.string.known, ScrollStateKeys.MESSAGES_KNOWN, knownFeedContentState), + MessagesTabItem(R.string.new_requests, ScrollStateKeys.MESSAGES_NEW, newFeedContentState), ) } } @@ -79,8 +75,8 @@ fun MessagesSinglePane( MessagesTabHeader( pagerState, tabs, - { markKnownAsRead.value = true }, - { markNewAsRead.value = true }, + { accountViewModel.markAllChatNotesAsRead(knownFeedContentState.visibleNotes()) }, + { accountViewModel.markAllChatNotesAsRead(newFeedContentState.visibleNotes()) }, ) } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt index 713a6f0716..0525c2bf99 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/twopane/ChatroomListPane.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -49,20 +48,17 @@ fun ChatroomList( ) { val pagerState = rememberPagerState { 2 } - val markKnownAsRead = remember { mutableStateOf(false) } - val markNewAsRead = remember { mutableStateOf(false) } - WatchLifecycleAndUpdateModel(knownFeedContentState) WatchLifecycleAndUpdateModel(newFeedContentState) ChatroomListFilterAssemblerSubscription(accountViewModel) val tabs by - remember(knownFeedContentState, markKnownAsRead) { + remember(knownFeedContentState) { derivedStateOf { listOf( - MessagesTabItem(R.string.known, ScrollStateKeys.MESSAGES_KNOWN, knownFeedContentState, markKnownAsRead), - MessagesTabItem(R.string.new_requests, ScrollStateKeys.MESSAGES_NEW, newFeedContentState, markNewAsRead), + MessagesTabItem(R.string.known, ScrollStateKeys.MESSAGES_KNOWN, knownFeedContentState), + MessagesTabItem(R.string.new_requests, ScrollStateKeys.MESSAGES_NEW, newFeedContentState), ) } } @@ -71,8 +67,8 @@ fun ChatroomList( MessagesTabHeader( pagerState, tabs, - { markKnownAsRead.value = true }, - { markNewAsRead.value = true }, + { accountViewModel.markAllChatNotesAsRead(knownFeedContentState.visibleNotes()) }, + { accountViewModel.markAllChatNotesAsRead(newFeedContentState.visibleNotes()) }, ) MessagesPager( From 1d9b2cffa23ae0549ca0f1bdcb78f0582373ba29 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Jul 2025 15:05:17 -0400 Subject: [PATCH 115/185] Filtering Addressable notes from Notification's note stack --- .../notifications/dal/NotificationFeedFilter.kt | 10 +++++++--- .../relay/client/single/basic/BasicRelayClient.kt | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index 07efff1bb7..bb85db4548 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.forks.forkFromVersion import com.vitorpamplona.quartz.experimental.forks.isForkFromAddressWithPubkey +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent @@ -74,8 +75,11 @@ class NotificationFeedFilter( val notifications = LocalCache.notes.filterIntoSet { _, note -> - acceptableEvent(note, filterParams) - } + note.event !is AddressableEvent && acceptableEvent(note, filterParams) + } + + LocalCache.addressables.filterIntoSet { _, note -> + acceptableEvent(note, filterParams) + } return sort(notifications) } @@ -126,7 +130,7 @@ class NotificationFeedFilter( it.event !is GiftWrapEvent && it.event !is PrivateTagArrayEvent && (it.event is LnZapEvent || notifAuthor != loggedInUserHex) && - (filterParams.isGlobal(it.relays) || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor) == true) && + (filterParams.isGlobal(it.relays) || notifAuthor == null || filterParams.isAuthorInFollows(notifAuthor)) && it.event?.isTaggedUser(loggedInUserHex) ?: false && (filterParams.isHiddenList || notifAuthor == null || !account.isHidden(notifAuthor)) && tagsAnEventByUser(it, loggedInUserHex) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 761ef648f7..2a5803871a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -310,6 +310,7 @@ open class BasicRelayClient( private fun processClosed(msg: ClosedMessage) { Log.w(logTag, "Relay Closed Subscription ${msg.subscriptionId} ${msg.message}") + stats.newNotice("Subscription closed: ${msg.subscriptionId} ${msg.message}") afterEOSEPerSubscription[msg.subscriptionId] = false listener.onClosed(this@BasicRelayClient, msg.subscriptionId, msg.message) } From 6d4708bbeb9ed0aa8f0cb79d04113323c6f7178a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Jul 2025 19:02:23 -0400 Subject: [PATCH 116/185] Implements a partial page system to load feeds. --- .../account/AccountFilterAssembler.kt | 2 + .../AccountFilterAssemblerSubscription.kt | 2 +- ...NotificationsEoseFromInboxRelaysManager.kt | 20 ++++- .../FilterNotificationsToPubkey.kt | 68 ++++++++++------- .../amethyst/ui/dal/FeedFilter.kt | 2 +- .../amethyst/ui/feeds/FeedContentState.kt | 8 ++ .../datasource/DiscoveryFilterAssembler.kt | 2 + .../DiscoveryFilterAssemblerSubscription.kt | 2 +- .../DiscoveryFollowsDiscoverySubAssembler1.kt | 18 ++++- .../DiscoveryFollowsDiscoverySubAssembler2.kt | 17 ++++- .../DiscoveryFollowsDiscoverySubAssembler3.kt | 16 +++- .../DiscoverLongFormFeedFilter.kt | 2 + .../nip23LongForm/SubAssemblyHelper.kt | 17 +++-- .../FilterLongFormByAllCommunities.kt | 3 +- .../subassemblies/FilterLongFormByAuthors.kt | 8 +- .../FilterLongFormByCommunity.kt | 3 +- .../subassemblies/FilterLongFormByFollows.kt | 3 +- .../subassemblies/FilterLongFormByGeohash.kt | 5 +- .../subassemblies/FilterLongFormByHashtag.kt | 5 +- .../subassemblies/FilterLongFormGlobal.kt | 5 +- .../nip28Chats/DiscoverChatFeedFilter.kt | 2 + .../discover/nip28Chats/SubAssemblyHelper.kt | 17 +++-- .../FilterPublicChatsByAllCommunities.kt | 3 +- .../FilterPublicChatsByAuthors.kt | 6 +- .../FilterPublicChatsByCommunity.kt | 3 +- .../FilterPublicChatsByFollows.kt | 3 +- .../FilterPublicChatsByGeohash.kt | 3 +- .../FilterPublicChatsByHashtag.kt | 3 +- .../subassemblies/FilterPublicChatsGlobal.kt | 3 +- .../nip51FollowSets/SubAssemblyHelper.kt | 17 +++-- .../FilterFollowSetsByAllCommunities.kt | 3 +- .../FilterFollowSetsByAuthors.kt | 6 +- .../FilterFollowSetsByCommunity.kt | 3 +- .../FilterFollowSetsByFollows.kt | 3 +- .../FilterFollowSetsByGeohash.kt | 3 +- .../FilterFollowSetsByHashtag.kt | 3 +- .../subassemblies/FilterFollowSetsGlobal.kt | 5 +- .../DiscoverLiveFeedFilter.kt | 2 + .../nip53LiveActivities/SubAssemblyHelper.kt | 17 +++-- .../FilterLiveActivitiesByAllCommunities.kt | 3 +- .../FilterLiveActivitiesByAuthors.kt | 6 +- .../FilterLiveActivitiesByCommunity.kt | 3 +- .../FilterLiveActivitiesByFollows.kt | 3 +- .../FilterLiveActivitiesByGeohash.kt | 3 +- .../FilterLiveActivitiesByHashtag.kt | 3 +- .../FilterLiveActivitiesGlobal.kt | 3 +- .../DiscoverCommunityFeedFilter.kt | 2 + .../nip72Communities/SubAssemblyHelper.kt | 17 +++-- .../FilterCommunitiesByAllCommunities.kt | 3 +- .../FilterCommunitiesByAuthors.kt | 6 +- .../FilterCommunitiesByCommunity.kt | 3 +- .../FilterCommunitiesByFollows.kt | 3 +- .../FilterCommunitiesByGeohash.kt | 3 +- .../FilterCommunitiesByHashtag.kt | 3 +- .../subassemblies/FilterCommunitiesGlobal.kt | 14 +++- .../nip90DVMs/DiscoverNIP89FeedFilter.kt | 2 + .../discover/nip90DVMs/SubAssemblyHelper.kt | 17 +++-- .../FilterContentDVMsByAllCommunities.kt | 3 +- .../FilterContentDVMsByAuthors.kt | 7 +- .../FilterContentDVMsByCommunity.kt | 3 +- .../FilterContentDVMsByFollows.kt | 3 +- .../FilterContentDVMsByGeohash.kt | 3 +- .../FilterContentDVMsByHashtag.kt | 3 +- .../subassemblies/FilterContentDVMsGlobal.kt | 3 +- .../DiscoverMarketplaceFeedFilter.kt | 2 + .../nip99Classifieds/SubAssemblyHelper.kt | 17 +++-- .../FilterClassifiedsByAllCommunities.kt | 3 +- .../FilterClassifiedsByAuthors.kt | 6 +- .../FilterClassifiedsByCommunity.kt | 3 +- .../FilterClassifiedsByFollows.kt | 3 +- .../FilterClassifiedsByGeohash.kt | 3 +- .../FilterClassifiedsByHashtag.kt | 3 +- .../subassemblies/FilterClassifiedsGlobal.kt | 3 +- .../home/datasource/HomeFilterAssembler.kt | 2 + .../HomeFilterAssemblerSubscription.kt | 6 +- .../nip01Core/FilterHomePostsByGeohashes.kt | 8 +- .../nip01Core/FilterHomePostsByGlobal.kt | 50 ++---------- .../nip01Core/FilterHomePostsByHashtags.kt | 4 +- .../nip22Comments/FilterPostsByScopes.kt | 2 +- .../FilterHomePostsByAllFollows.kt | 17 +++-- .../nip65Follows/FilterHomePostsByAuthors.kt | 76 ++++++++++++------- .../HomeOutboxEventsEoseManager.kt | 31 +++++--- .../FilterHomePostsByAllCommunities.kt | 3 +- .../FilterHomePostsFromCommunities.kt | 3 +- .../notifications/CardFeedContentState.kt | 8 ++ .../loggedIn/video/dal/VideoFeedFilter.kt | 21 +++-- .../video/datasource/VideoFilterAssembler.kt | 2 + .../VideoFilterAssemblerSubscription.kt | 2 +- .../VideoOutboxEventsFilterSubAssembler.kt | 24 +++--- .../FilterPictureAndVideoByGeohash.kt | 7 +- .../FilterPictureAndVideoByHashtag.kt | 7 +- .../nip01Core/FilterPictureAndVideoGlobal.kt | 7 +- .../FilterPictureAndVideoByAuthors.kt | 11 ++- .../FilterPictureAndVideoByFollows.kt | 19 ++--- .../FilterPictureAndVideoByAllCommunities.kt | 11 +-- .../FilterPictureAndVideoByCommunity.kt | 11 +-- 96 files changed, 511 insertions(+), 299 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt index 6795ee287f..479d3c73ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssembler.kt @@ -26,12 +26,14 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.metadat import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromInboxRelaysManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01Notifications.AccountNotificationsEoseFromRandomRelaysManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip59GiftWraps.AccountGiftWrapsEoseManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient // This allows multiple screen to be listening to logged-in accounts. class AccountQueryState( val account: Account, + val feedContentStates: AccountFeedContentStates, val otherAccounts: Set, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt index 4c201bcce7..cae7379f6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/AccountFilterAssemblerSubscription.kt @@ -37,7 +37,7 @@ fun AccountFilterAssemblerSubscription( // even if they are tracking the same tag. val state = remember(accountViewModel) { - AccountQueryState(accountViewModel.account, accountViewModel.trustedAccounts.value) + AccountQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.trustedAccounts.value) } KeyDataSourceSubscription(state, dataSource) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt index d4f2ff05ab..f9d36d2d32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromInboxRelaysManager.kt @@ -27,10 +27,12 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class AccountNotificationsEoseFromInboxRelaysManager( @@ -49,7 +51,16 @@ class AccountNotificationsEoseFromInboxRelaysManager( since: SincePerRelayMap?, ): List? = key.account.notificationRelays.flow.value.flatMap { - filterNotificationsToPubkey(it, user(key).pubkeyHex, since?.get(it)?.time) + filterSummaryNotificationsToPubkey( + relay = it, + pubkey = user(key).pubkeyHex, + since = since?.get(it)?.time ?: TimeUtils.oneWeekAgo(), + ) + filterNotificationsToPubkey( + relay = it, + pubkey = user(key).pubkeyHex, + since = since?.get(it)?.time ?: key.feedContentStates.notifications.lastNoteCreatedAtIfFilled(), + ) } val userJobMap = mutableMapOf>() @@ -61,7 +72,12 @@ class AccountNotificationsEoseFromInboxRelaysManager( userJobMap[user] = listOf( key.account.scope.launch(Dispatchers.Default) { - key.account.notificationRelays.flow.collectLatest { + key.account.notificationRelays.flow.sample(1000).collectLatest { + invalidateFilters() + } + }, + key.account.scope.launch(Dispatchers.Default) { + key.feedContentStates.notifications.lastNoteCreatedAtWhenFullyLoaded.sample(1000).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt index 16614cbb68..60b3377649 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt @@ -46,18 +46,23 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -val NotificationsPerKeyKinds = +val SummaryKinds = listOf( TextNoteEvent.KIND, ReactionEvent.KIND, RepostEvent.KIND, GenericRepostEvent.KIND, - ReportEvent.KIND, LnZapEvent.KIND, + ) + +val NotificationsPerKeyKinds = + listOf( + ReportEvent.KIND, LnZapPaymentResponseEvent.KIND, ChannelMessageEvent.KIND, EphemeralChatEvent.KIND, BadgeAwardEvent.KIND, + PollNoteEvent.KIND, ) val NotificationsPerKeyKinds2 = @@ -74,7 +79,26 @@ val NotificationsPerKeyKinds2 = InteractiveStorySceneEvent.KIND, ) -val NotificationsPerKeyKinds3 = listOf(PollNoteEvent.KIND) +fun filterSummaryNotificationsToPubkey( + relay: NormalizedRelayUrl, + pubkey: HexKey?, + since: Long?, +): List { + if (pubkey == null || pubkey.isEmpty()) return emptyList() + + return listOf( + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = SummaryKinds, + tags = mapOf("p" to listOf(pubkey)), + limit = if (since == null) 2000 else null, + since = since, + ), + ), + ) +} fun filterNotificationsToPubkey( relay: NormalizedRelayUrl, @@ -90,7 +114,7 @@ fun filterNotificationsToPubkey( Filter( kinds = NotificationsPerKeyKinds, tags = mapOf("p" to listOf(pubkey)), - limit = 2000, + limit = if (since == null) 50 else null, since = since, ), ), @@ -100,17 +124,7 @@ fun filterNotificationsToPubkey( Filter( kinds = NotificationsPerKeyKinds2, tags = mapOf("p" to listOf(pubkey)), - limit = 50, - since = since, - ), - ), - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = NotificationsPerKeyKinds3, - tags = mapOf("p" to listOf(pubkey)), - limit = 100, + limit = if (since == null) 100 else null, since = since, ), ), @@ -129,29 +143,29 @@ fun filterJustTheLatestNotificationsToPubkeyFromRandomRelays( relay = relay, filter = Filter( - kinds = NotificationsPerKeyKinds, + kinds = SummaryKinds, tags = mapOf("p" to listOf(pubkey)), limit = 10, since = since, ), ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = NotificationsPerKeyKinds, + tags = mapOf("p" to listOf(pubkey)), + limit = 5, + since = since, + ), + ), RelayBasedFilter( relay = relay, filter = Filter( kinds = NotificationsPerKeyKinds2, tags = mapOf("p" to listOf(pubkey)), - limit = 10, - since = since, - ), - ), - RelayBasedFilter( - relay = relay, - filter = - Filter( - kinds = NotificationsPerKeyKinds3, - tags = mapOf("p" to listOf(pubkey)), - limit = 10, + limit = 5, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt index 5f0df79fe4..d6a71a6c2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FeedFilter.kt @@ -32,7 +32,7 @@ abstract class FeedFilter { return feed.take(limit()) } - open fun limit() = 1000 + open fun limit() = 500 /** Returns a string that serves as the key to invalidate the list if it changes. */ abstract fun feedKey(): Any diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt index e6b9dccc05..33e36bdd3c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt @@ -57,6 +57,8 @@ class FeedContentState( override val isRefreshing: MutableState = mutableStateOf(false) + val lastNoteCreatedAtWhenFullyLoaded = MutableStateFlow(null) + fun sendToTop() { if (scrolltoTopPending) return @@ -81,6 +83,8 @@ class FeedContentState( } } + fun lastNoteCreatedAtIfFilled() = lastNoteCreatedAtWhenFullyLoaded.value + fun refreshSuspended() { checkNotInMainThread() @@ -103,6 +107,10 @@ class FeedContentState( } private fun updateFeed(notes: ImmutableList) { + if (notes.size >= localFilter.limit()) { + lastNoteCreatedAtWhenFullyLoaded.tryEmit(notes.lastOrNull { it.event != null }?.createdAt()) + } + val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.tryEmit(FeedState.Empty) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt index b3a9c8f08f..c86d924c80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt @@ -22,12 +22,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import kotlinx.coroutines.CoroutineScope // This allows multiple screen to be listening to tags, even the same tag class DiscoveryQueryState( val account: Account, + val feedStates: AccountFeedContentStates, val scope: CoroutineScope, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt index b156df1cf1..5a2a10b0c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssemblerSubscription.kt @@ -43,7 +43,7 @@ fun DiscoveryFilterAssemblerSubscription( // even if they are tracking the same tag. val state = remember(accountViewModel.account) { - DiscoveryQueryState(accountViewModel.account, accountViewModel.viewModelScope) + DiscoveryQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope) } KeyDataSourceSubscription(state, dataSource) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt index ef35137a63..7c3ef0db6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch @@ -46,9 +47,9 @@ class DiscoveryFollowsDiscoverySubAssembler1( ): List? { val feedSettings = key.followsPerRelay() - return makeLongFormFilter(feedSettings, since) + - makeClassifiedsFilter(feedSettings, since) + - makeContentDVMsFilter(feedSettings, since) + return makeLongFormFilter(feedSettings, since, key.feedStates.discoverReads.lastNoteCreatedAtIfFilled()) + + makeClassifiedsFilter(feedSettings, since, key.feedStates.discoverMarketplace.lastNoteCreatedAtIfFilled()) + + makeContentDVMsFilter(feedSettings, since, key.feedStates.discoverDVMs.lastNoteCreatedAtIfFilled()) } override fun user(key: DiscoveryQueryState) = key.account.userProfile() @@ -81,6 +82,17 @@ class DiscoveryFollowsDiscoverySubAssembler1( invalidateFilters() } }, + key.account.scope.launch(Dispatchers.Default) { + combine( + key.feedStates.discoverReads.lastNoteCreatedAtWhenFullyLoaded, + key.feedStates.discoverDVMs.lastNoteCreatedAtWhenFullyLoaded, + key.feedStates.discoverMarketplace.lastNoteCreatedAtWhenFullyLoaded, + ) { + Any() + }.sample(1000).collectLatest { + invalidateFilters() + } + }, ) return super.newSub(key) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt index e3c560c9fa..2cf987317c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch @@ -45,8 +46,8 @@ class DiscoveryFollowsDiscoverySubAssembler2( ): List? { val feedSettings = key.followsPerRelay() - return makeFollowSetsFilter(feedSettings, since) + - makeLiveActivitiesFilter(feedSettings, since) + return makeFollowSetsFilter(feedSettings, since, key.feedStates.discoverFollowSets.lastNoteCreatedAtIfFilled()) + + makeLiveActivitiesFilter(feedSettings, since, key.feedStates.discoverLive.lastNoteCreatedAtIfFilled()) } override fun user(key: DiscoveryQueryState) = key.account.userProfile() @@ -75,7 +76,17 @@ class DiscoveryFollowsDiscoverySubAssembler2( } }, key.scope.launch(Dispatchers.Default) { - key.followsPerRelayFlow().sample(500).collectLatest { + key.followsPerRelayFlow().sample(1000).collectLatest { + invalidateFilters() + } + }, + key.account.scope.launch(Dispatchers.Default) { + combine( + key.feedStates.discoverFollowSets.lastNoteCreatedAtWhenFullyLoaded, + key.feedStates.discoverLive.lastNoteCreatedAtWhenFullyLoaded, + ) { + Any() + }.sample(1000).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt index 5b945fd5e4..660896e6c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch @@ -44,9 +45,8 @@ class DiscoveryFollowsDiscoverySubAssembler3( since: SincePerRelayMap?, ): List? { val feedSettings = key.followsPerRelay() - - return makePublicChatsFilter(feedSettings, since) + - makeCommunitiesFilter(feedSettings, since) + return makePublicChatsFilter(feedSettings, since, key.feedStates.discoverPublicChats.lastNoteCreatedAtIfFilled()) + + makeCommunitiesFilter(feedSettings, since, key.feedStates.discoverCommunities.lastNoteCreatedAtIfFilled()) } override fun user(key: DiscoveryQueryState) = key.account.userProfile() @@ -79,6 +79,16 @@ class DiscoveryFollowsDiscoverySubAssembler3( invalidateFilters() } }, + key.account.scope.launch(Dispatchers.Default) { + combine( + key.feedStates.discoverPublicChats.lastNoteCreatedAtWhenFullyLoaded, + key.feedStates.discoverCommunities.lastNoteCreatedAtWhenFullyLoaded, + ) { + Any() + }.sample(1000).collectLatest { + invalidateFilters() + } + }, ) return super.newSub(key) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt index e1c5b66ab1..0eea42a298 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/DiscoverLongFormFeedFilter.kt @@ -35,6 +35,8 @@ open class DiscoverLongFormFeedFilter( ) : AdditiveFeedFilter() { override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList() + override fun limit() = 100 + open fun followList(): String = account.settings.defaultDiscoveryFollowList.value override fun showHiddenKey(): Boolean = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt index aea7bdacff..6e858f8b69 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt @@ -42,15 +42,16 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeLongFormFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List = when (feedSettings) { - is AllCommunitiesTopNavPerRelayFilterSet -> filterLongFormByAllCommunities(feedSettings, since) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterLongFormByFollows(feedSettings, since) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since) - is GlobalTopNavPerRelayFilterSet -> filterLongFormGlobal(feedSettings, since) - is HashtagTopNavPerRelayFilterSet -> filterLongFormByHashtag(feedSettings, since) - is LocationTopNavPerRelayFilterSet -> filterLongFormByGeohash(feedSettings, since) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since) - is SingleCommunityTopNavPerRelayFilterSet -> filterLongFormByCommunity(feedSettings, since) + is AllCommunitiesTopNavPerRelayFilterSet -> filterLongFormByAllCommunities(feedSettings, since, defaultSince) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterLongFormByFollows(feedSettings, since, defaultSince) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since, defaultSince) + is GlobalTopNavPerRelayFilterSet -> filterLongFormGlobal(feedSettings, since, defaultSince) + is HashtagTopNavPerRelayFilterSet -> filterLongFormByHashtag(feedSettings, since, defaultSince) + is LocationTopNavPerRelayFilterSet -> filterLongFormByGeohash(feedSettings, since, defaultSince) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since, defaultSince) + is SingleCommunityTopNavPerRelayFilterSet -> filterLongFormByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt index 5a653f22aa..44413e7ba6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAllCommunities.kt @@ -68,6 +68,7 @@ fun filterLongFormAllCommunities( fun filterLongFormByAllCommunities( communitySet: AllCommunitiesTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -76,7 +77,7 @@ fun filterLongFormByAllCommunities( filterLongFormAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt index 82cf18398b..eb1b082907 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt @@ -42,7 +42,7 @@ fun filterLongFormAuthors( Filter( authors = authorList, kinds = listOf(LongTextNoteEvent.KIND), - limit = 30, + limit = 200, since = since, ), ), @@ -52,6 +52,7 @@ fun filterLongFormAuthors( fun filterLongFormByAuthors( authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -63,7 +64,7 @@ fun filterLongFormByAuthors( filterLongFormAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() @@ -72,6 +73,7 @@ fun filterLongFormByAuthors( fun filterLongFormByAuthors( authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -83,7 +85,7 @@ fun filterLongFormByAuthors( filterLongFormAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt index 709c1d5deb..af7f7b963d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByCommunity.kt @@ -70,6 +70,7 @@ fun filterLongFormByCommunity( fun filterLongFormByCommunity( communitySet: SingleCommunityTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -79,7 +80,7 @@ fun filterLongFormByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt index e86af94c60..b928aad205 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt @@ -27,11 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterLongFormByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (followsSet.set.isEmpty()) return emptyList() return followsSet.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt index 2fad478f3d..2346b02a7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByGeohash.kt @@ -44,7 +44,7 @@ fun filterLongFormByGeohash( Filter( kinds = listOf(LongTextNoteEvent.KIND), tags = mapOf("g" to geoHashes), - limit = 30, + limit = 100, since = since, ), ), @@ -54,6 +54,7 @@ fun filterLongFormByGeohash( fun filterLongFormByGeohash( geoSet: LocationTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (geoSet.set.isEmpty()) return emptyList() @@ -65,7 +66,7 @@ fun filterLongFormByGeohash( filterLiveActivitiesByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt index 4ef4daa208..26dae5a7cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByHashtag.kt @@ -44,7 +44,7 @@ fun filterLongFormByHashtag( Filter( kinds = listOf(LongTextNoteEvent.KIND), tags = mapOf("t" to hashtags), - limit = 30, + limit = 200, since = since, ), ), @@ -54,6 +54,7 @@ fun filterLongFormByHashtag( fun filterLongFormByHashtag( hashSet: HashtagTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (hashSet.set.isEmpty()) return emptyList() @@ -65,7 +66,7 @@ fun filterLongFormByHashtag( filterLongFormByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt index 09ad32c4aa..1c74098a40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt @@ -29,17 +29,18 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent fun filterLongFormGlobal( relays: GlobalTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (relays.set.isEmpty()) return emptyList() return relays.set.map { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince RelayBasedFilter( relay = it.key, filter = Filter( kinds = listOf(LongTextNoteEvent.KIND), - limit = 30, + limit = 100, since = since, ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt index 1fe527ce0d..c7c4e9ca5e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/DiscoverChatFeedFilter.kt @@ -35,6 +35,8 @@ open class DiscoverChatFeedFilter( ) : AdditiveFeedFilter() { override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultDiscoveryFollowList.value + override fun limit() = 100 + override fun showHiddenKey(): Boolean = account.settings.defaultDiscoveryFollowList.value == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt index 4b79b94a2f..2425467aa8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt @@ -42,15 +42,16 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makePublicChatsFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List = when (feedSettings) { - is AllCommunitiesTopNavPerRelayFilterSet -> filterPublicChatsByAllCommunities(feedSettings, since) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByFollows(feedSettings, since) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since) - is GlobalTopNavPerRelayFilterSet -> filterPublicChatsGlobal(feedSettings, since) - is HashtagTopNavPerRelayFilterSet -> filterPublicChatsByHashtag(feedSettings, since) - is LocationTopNavPerRelayFilterSet -> filterPublicChatsByGeohash(feedSettings, since) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since) - is SingleCommunityTopNavPerRelayFilterSet -> filterPublicChatsByCommunity(feedSettings, since) + is AllCommunitiesTopNavPerRelayFilterSet -> filterPublicChatsByAllCommunities(feedSettings, since, defaultSince) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByFollows(feedSettings, since, defaultSince) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since, defaultSince) + is GlobalTopNavPerRelayFilterSet -> filterPublicChatsGlobal(feedSettings, since, defaultSince) + is HashtagTopNavPerRelayFilterSet -> filterPublicChatsByHashtag(feedSettings, since, defaultSince) + is LocationTopNavPerRelayFilterSet -> filterPublicChatsByGeohash(feedSettings, since, defaultSince) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since, defaultSince) + is SingleCommunityTopNavPerRelayFilterSet -> filterPublicChatsByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt index 7b3092f74c..437237ba11 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAllCommunities.kt @@ -75,6 +75,7 @@ fun filterPublicChatsAllCommunities( fun filterPublicChatsByAllCommunities( communitySet: AllCommunitiesTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() return communitySet.set @@ -82,7 +83,7 @@ fun filterPublicChatsByAllCommunities( filterPublicChatsAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt index aea997e2c4..334502d052 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt @@ -59,6 +59,7 @@ fun filterPublicChatsAuthors( fun filterPublicChatsByAuthors( authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -70,7 +71,7 @@ fun filterPublicChatsByAuthors( filterPublicChatsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() @@ -79,6 +80,7 @@ fun filterPublicChatsByAuthors( fun filterPublicChatsByAuthors( authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -90,7 +92,7 @@ fun filterPublicChatsByAuthors( filterPublicChatsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt index 6976a0940d..020916f4cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByCommunity.kt @@ -77,6 +77,7 @@ fun filterPublicChatsByCommunity( fun filterPublicChatsByCommunity( communitySet: SingleCommunityTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -86,7 +87,7 @@ fun filterPublicChatsByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt index 6da7b58e9d..d527dbbfaa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt @@ -27,11 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterPublicChatsByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (followsSet.set.isEmpty()) return emptyList() return followsSet.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt index 1804ef29ef..778834c649 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByGeohash.kt @@ -60,6 +60,7 @@ fun filterPublicChatsByGeohash( fun filterPublicChatsByGeohash( geoSet: LocationTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (geoSet.set.isEmpty()) return emptyList() @@ -71,7 +72,7 @@ fun filterPublicChatsByGeohash( filterPublicChatsByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt index 2138760b00..a2d147ad12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByHashtag.kt @@ -61,6 +61,7 @@ fun filterPublicChatsByHashtag( fun filterPublicChatsByHashtag( hashSet: HashtagTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (hashSet.set.isEmpty()) return emptyList() @@ -72,7 +73,7 @@ fun filterPublicChatsByHashtag( filterPublicChatsByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt index 3959aa50d3..ce424a1c7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt @@ -31,11 +31,12 @@ import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent fun filterPublicChatsGlobal( relays: GlobalTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (relays.set.isEmpty()) return emptyList() return relays.set.map { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince RelayBasedFilter( relay = it.key, filter = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt index 8c3797df9a..e88f3c0c3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt @@ -42,15 +42,16 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeFollowSetsFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List = when (feedSettings) { - is AllCommunitiesTopNavPerRelayFilterSet -> filterFollowSetsByAllCommunities(feedSettings, since) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByFollows(feedSettings, since) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since) - is GlobalTopNavPerRelayFilterSet -> filterFollowSetsGlobal(feedSettings, since) - is HashtagTopNavPerRelayFilterSet -> filterFollowSetsByHashtag(feedSettings, since) - is LocationTopNavPerRelayFilterSet -> filterFollowSetsByGeohash(feedSettings, since) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since) - is SingleCommunityTopNavPerRelayFilterSet -> filterFollowSetsByCommunity(feedSettings, since) + is AllCommunitiesTopNavPerRelayFilterSet -> filterFollowSetsByAllCommunities(feedSettings, since, defaultSince) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByFollows(feedSettings, since, defaultSince) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since, defaultSince) + is GlobalTopNavPerRelayFilterSet -> filterFollowSetsGlobal(feedSettings, since, defaultSince) + is HashtagTopNavPerRelayFilterSet -> filterFollowSetsByHashtag(feedSettings, since, defaultSince) + is LocationTopNavPerRelayFilterSet -> filterFollowSetsByGeohash(feedSettings, since, defaultSince) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since, defaultSince) + is SingleCommunityTopNavPerRelayFilterSet -> filterFollowSetsByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt index 745746ba8b..865adb4d33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAllCommunities.kt @@ -69,6 +69,7 @@ fun filterFollowSetsAllCommunities( fun filterFollowSetsByAllCommunities( communitySet: AllCommunitiesTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -77,7 +78,7 @@ fun filterFollowSetsByAllCommunities( filterContentDVMsAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt index 9c657837db..f9fed40837 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt @@ -52,6 +52,7 @@ fun filterFollowSetsAuthors( fun filterFollowSetsByAuthors( authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -63,7 +64,7 @@ fun filterFollowSetsByAuthors( filterFollowSetsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() @@ -72,6 +73,7 @@ fun filterFollowSetsByAuthors( fun filterFollowSetsByAuthors( authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -83,7 +85,7 @@ fun filterFollowSetsByAuthors( filterFollowSetsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt index 47c061454b..fe8e661bf4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByCommunity.kt @@ -70,6 +70,7 @@ fun filterFollowSetsByCommunity( fun filterFollowSetsByCommunity( communitySet: SingleCommunityTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -79,7 +80,7 @@ fun filterFollowSetsByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt index 6dcb143370..a7dbfff209 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt @@ -27,11 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterFollowSetsByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (followsSet.set.isEmpty()) return emptyList() return followsSet.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt index 236a58e3a2..b6ff52cc42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByGeohash.kt @@ -53,6 +53,7 @@ fun filterFollowSetsByGeohash( fun filterFollowSetsByGeohash( geoSet: LocationTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (geoSet.set.isEmpty()) return emptyList() @@ -64,7 +65,7 @@ fun filterFollowSetsByGeohash( filterFollowSetsByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt index aa7d74d43f..1301091733 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByHashtag.kt @@ -54,6 +54,7 @@ fun filterFollowSetsByHashtag( fun filterFollowSetsByHashtag( hashSet: HashtagTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (hashSet.set.isEmpty()) return emptyList() @@ -65,7 +66,7 @@ fun filterFollowSetsByHashtag( filterFollowSetsByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt index 0feb24def2..7684ecd79f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt @@ -29,17 +29,18 @@ import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent fun filterFollowSetsGlobal( relays: GlobalTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (relays.set.isEmpty()) return emptyList() return relays.set.map { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince RelayBasedFilter( relay = it.key, filter = Filter( kinds = listOf(FollowListEvent.KIND), - limit = 30, + limit = 300, since = since, ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt index 3c60054a10..475b27b894 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt @@ -42,6 +42,8 @@ open class DiscoverLiveFeedFilter( open fun followList(): String = account.settings.defaultDiscoveryFollowList.value + override fun limit() = 50 + override fun showHiddenKey(): Boolean = followList() == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) || followList() == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt index 0cca444f94..d91a8e598c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt @@ -42,15 +42,16 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeLiveActivitiesFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List = when (feedSettings) { - is AllCommunitiesTopNavPerRelayFilterSet -> filterLiveActivitiesByAllCommunities(feedSettings, since) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByFollows(feedSettings, since) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since) - is GlobalTopNavPerRelayFilterSet -> filterLiveActivitiesGlobal(feedSettings, since) - is HashtagTopNavPerRelayFilterSet -> filterLiveActivitiesByHashtag(feedSettings, since) - is LocationTopNavPerRelayFilterSet -> filterLiveActivitiesByGeohash(feedSettings, since) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since) - is SingleCommunityTopNavPerRelayFilterSet -> filterLiveActivitiesByCommunity(feedSettings, since) + is AllCommunitiesTopNavPerRelayFilterSet -> filterLiveActivitiesByAllCommunities(feedSettings, since, defaultSince) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByFollows(feedSettings, since, defaultSince) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since, defaultSince) + is GlobalTopNavPerRelayFilterSet -> filterLiveActivitiesGlobal(feedSettings, since, defaultSince) + is HashtagTopNavPerRelayFilterSet -> filterLiveActivitiesByHashtag(feedSettings, since, defaultSince) + is LocationTopNavPerRelayFilterSet -> filterLiveActivitiesByGeohash(feedSettings, since, defaultSince) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since, defaultSince) + is SingleCommunityTopNavPerRelayFilterSet -> filterLiveActivitiesByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt index 3dc411bc4d..b102e7b24b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAllCommunities.kt @@ -70,6 +70,7 @@ fun filterLiveActivitiesAllCommunities( fun filterLiveActivitiesByAllCommunities( communitySet: AllCommunitiesTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -78,7 +79,7 @@ fun filterLiveActivitiesByAllCommunities( filterContentDVMsAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt index 22d03b22ae..f449224a0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt @@ -64,6 +64,7 @@ fun filterLiveActivitiesAuthors( fun filterLiveActivitiesByAuthors( authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -75,7 +76,7 @@ fun filterLiveActivitiesByAuthors( filterLiveActivitiesAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() @@ -84,6 +85,7 @@ fun filterLiveActivitiesByAuthors( fun filterLiveActivitiesByAuthors( authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -95,7 +97,7 @@ fun filterLiveActivitiesByAuthors( filterLiveActivitiesAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt index c87e918806..85dd03f564 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByCommunity.kt @@ -71,6 +71,7 @@ fun filterLiveActivitiesByCommunity( fun filterLiveActivitiesByCommunity( communitySet: SingleCommunityTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -80,7 +81,7 @@ fun filterLiveActivitiesByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt index 1e8cbb58bb..34d3bff28d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt @@ -27,11 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterLiveActivitiesByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (followsSet.set.isEmpty()) return emptyList() return followsSet.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt index 62471f337f..7c2d11dbc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByGeohash.kt @@ -54,6 +54,7 @@ fun filterLiveActivitiesByGeohash( fun filterLiveActivitiesByGeohash( geoSet: LocationTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (geoSet.set.isEmpty()) return emptyList() @@ -65,7 +66,7 @@ fun filterLiveActivitiesByGeohash( filterLiveActivitiesByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt index 5193a653e6..5367d860b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByHashtag.kt @@ -55,6 +55,7 @@ fun filterLiveActivitiesByHashtag( fun filterLiveActivitiesByHashtag( hashSet: HashtagTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (hashSet.set.isEmpty()) return emptyList() @@ -66,7 +67,7 @@ fun filterLiveActivitiesByHashtag( filterLiveActivitiesByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt index 7252d171fd..d3cb8095d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt @@ -30,11 +30,12 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEven fun filterLiveActivitiesGlobal( relays: GlobalTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (relays.set.isEmpty()) return emptyList() return relays.set.map { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince RelayBasedFilter( relay = it.key, filter = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt index 771da9c840..6d28ea78dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/DiscoverCommunityFeedFilter.kt @@ -37,6 +37,8 @@ open class DiscoverCommunityFeedFilter( ) : AdditiveFeedFilter() { override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultDiscoveryFollowList.value + override fun limit() = 150 + override fun showHiddenKey(): Boolean = account.settings.defaultDiscoveryFollowList.value == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt index 4b275fae7e..7d153d2b58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt @@ -42,15 +42,16 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeCommunitiesFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List = when (feedSettings) { - is AllCommunitiesTopNavPerRelayFilterSet -> filterCommunitiesByAllCommunities(feedSettings, since) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByFollows(feedSettings, since) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since) - is GlobalTopNavPerRelayFilterSet -> filterCommunitiesGlobal(feedSettings, since) - is HashtagTopNavPerRelayFilterSet -> filterCommunitiesByHashtag(feedSettings, since) - is LocationTopNavPerRelayFilterSet -> filterCommunitiesByGeohash(feedSettings, since) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since) - is SingleCommunityTopNavPerRelayFilterSet -> filterCommunitiesByCommunity(feedSettings, since) + is AllCommunitiesTopNavPerRelayFilterSet -> filterCommunitiesByAllCommunities(feedSettings, since, defaultSince) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByFollows(feedSettings, since, defaultSince) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since, defaultSince) + is GlobalTopNavPerRelayFilterSet -> filterCommunitiesGlobal(feedSettings, since, defaultSince) + is HashtagTopNavPerRelayFilterSet -> filterCommunitiesByHashtag(feedSettings, since, defaultSince) + is LocationTopNavPerRelayFilterSet -> filterCommunitiesByGeohash(feedSettings, since, defaultSince) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since, defaultSince) + is SingleCommunityTopNavPerRelayFilterSet -> filterCommunitiesByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt index da3004e2d7..7c2112abf1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAllCommunities.kt @@ -64,6 +64,7 @@ fun filterCommunitiesAllCommunities( fun filterCommunitiesByAllCommunities( communitySet: AllCommunitiesTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -72,7 +73,7 @@ fun filterCommunitiesByAllCommunities( filterCommunitiesAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt index 6069271358..28abab14d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt @@ -53,6 +53,7 @@ fun filterCommunitiesAuthors( fun filterCommunitiesByAuthors( authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -64,7 +65,7 @@ fun filterCommunitiesByAuthors( filterCommunitiesAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() @@ -73,6 +74,7 @@ fun filterCommunitiesByAuthors( fun filterCommunitiesByAuthors( authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -84,7 +86,7 @@ fun filterCommunitiesByAuthors( filterCommunitiesAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt index 92be0c095e..fe3872fd80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByCommunity.kt @@ -53,6 +53,7 @@ fun filterClassifiedsByCommunity( fun filterCommunitiesByCommunity( communitySet: SingleCommunityTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -62,7 +63,7 @@ fun filterCommunitiesByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt index 7d233eaefb..faa840d474 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt @@ -27,11 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterCommunitiesByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (followsSet.set.isEmpty()) return emptyList() return followsSet.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt index 4a8576855a..6c9939b580 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByGeohash.kt @@ -54,6 +54,7 @@ fun filterCommunitiesByGeohash( fun filterCommunitiesByGeohash( geoSet: LocationTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (geoSet.set.isEmpty()) return emptyList() @@ -65,7 +66,7 @@ fun filterCommunitiesByGeohash( filterCommunitiesByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt index 13074bde1f..0a20b39fde 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByHashtag.kt @@ -55,6 +55,7 @@ fun filterCommunitiesByHashtag( fun filterCommunitiesByHashtag( hashSet: HashtagTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (hashSet.set.isEmpty()) return emptyList() @@ -66,7 +67,7 @@ fun filterCommunitiesByHashtag( filterCommunitiesByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt index 6f79e2f79c..a9baaba88c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt @@ -30,17 +30,27 @@ import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefiniti fun filterCommunitiesGlobal( relays: GlobalTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (relays.set.isEmpty()) return emptyList() return relays.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince listOf( RelayBasedFilter( relay = it.key, filter = Filter( - kinds = listOf(CommunityDefinitionEvent.KIND, CommunityPostApprovalEvent.KIND), + kinds = listOf(CommunityDefinitionEvent.KIND), + limit = 100, + since = since, + ), + ), + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = listOf(CommunityPostApprovalEvent.KIND), limit = 30, since = since, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt index d2f3e9a7b4..2a697c56cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/DiscoverNIP89FeedFilter.kt @@ -38,6 +38,8 @@ open class DiscoverNIP89FeedFilter( override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList() + override fun limit() = 50 + open fun followList(): String = account.settings.defaultDiscoveryFollowList.value override fun showHiddenKey(): Boolean = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt index 1a03971c5f..324c1902fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt @@ -42,15 +42,16 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeContentDVMsFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long?, ): List = when (feedSettings) { - is AllCommunitiesTopNavPerRelayFilterSet -> filterContentDVMsByAllCommunities(feedSettings, since) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByFollows(feedSettings, since) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since) - is GlobalTopNavPerRelayFilterSet -> filterContentDVMsGlobal(feedSettings, since) - is HashtagTopNavPerRelayFilterSet -> filterContentDVMsByHashtag(feedSettings, since) - is LocationTopNavPerRelayFilterSet -> filterContentDVMsByGeohash(feedSettings, since) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since) - is SingleCommunityTopNavPerRelayFilterSet -> filterContentDVMsByCommunity(feedSettings, since) + is AllCommunitiesTopNavPerRelayFilterSet -> filterContentDVMsByAllCommunities(feedSettings, since, defaultSince) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByFollows(feedSettings, since, defaultSince) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since, defaultSince) + is GlobalTopNavPerRelayFilterSet -> filterContentDVMsGlobal(feedSettings, since, defaultSince) + is HashtagTopNavPerRelayFilterSet -> filterContentDVMsByHashtag(feedSettings, since, defaultSince) + is LocationTopNavPerRelayFilterSet -> filterContentDVMsByGeohash(feedSettings, since, defaultSince) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since, defaultSince) + is SingleCommunityTopNavPerRelayFilterSet -> filterContentDVMsByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt index 70ae780cd3..89dfd866b7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAllCommunities.kt @@ -68,6 +68,7 @@ fun filterContentDVMsAllCommunities( fun filterContentDVMsByAllCommunities( communitySet: AllCommunitiesTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -76,7 +77,7 @@ fun filterContentDVMsByAllCommunities( filterContentDVMsAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt index 25c3a031ec..7ede517e3c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import kotlin.collections.flatten fun filterContentDVMsAuthors( relay: NormalizedRelayUrl, @@ -53,6 +54,7 @@ fun filterContentDVMsAuthors( fun filterContentDVMsByAuthors( authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -64,7 +66,7 @@ fun filterContentDVMsByAuthors( filterContentDVMsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() @@ -73,6 +75,7 @@ fun filterContentDVMsByAuthors( fun filterContentDVMsByAuthors( authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -84,7 +87,7 @@ fun filterContentDVMsByAuthors( filterContentDVMsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt index ed61e54797..82c25e9b60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByCommunity.kt @@ -70,6 +70,7 @@ fun filterContentDVMsByCommunity( fun filterContentDVMsByCommunity( communitySet: SingleCommunityTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -79,7 +80,7 @@ fun filterContentDVMsByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt index 82fff15cb0..3ea718586c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt @@ -27,11 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterContentDVMsByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (followsSet.set.isEmpty()) return emptyList() return followsSet.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt index 871e96151c..55bd2db38d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByGeohash.kt @@ -53,6 +53,7 @@ fun filterContentDVMsByGeohash( fun filterContentDVMsByGeohash( geoSet: LocationTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (geoSet.set.isEmpty()) return emptyList() @@ -64,7 +65,7 @@ fun filterContentDVMsByGeohash( filterContentDVMsByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt index 9150045172..2920bd956d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByHashtag.kt @@ -54,6 +54,7 @@ fun filterContentDVMsByHashtag( fun filterContentDVMsByHashtag( hashSet: HashtagTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (hashSet.set.isEmpty()) return emptyList() @@ -65,7 +66,7 @@ fun filterContentDVMsByHashtag( filterContentDVMsByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt index 00e94d9e07..395ca8d121 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsGlobal.kt @@ -29,11 +29,12 @@ import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent fun filterContentDVMsGlobal( relays: GlobalTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (relays.set.isEmpty()) return emptyList() return relays.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince listOf( RelayBasedFilter( relay = it.key, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt index 038c2bbc92..66793384a6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/DiscoverMarketplaceFeedFilter.kt @@ -36,6 +36,8 @@ open class DiscoverMarketplaceFeedFilter( open fun followList(): String = account.settings.defaultDiscoveryFollowList.value + override fun limit() = 150 + override fun showHiddenKey(): Boolean = followList() == PeopleListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) || followList() == MuteListEvent.Companion.blockListFor(account.userProfile().pubkeyHex) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt index fca10fdc3f..91928399ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt @@ -42,15 +42,16 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun makeClassifiedsFilter( feedSettings: IFeedTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List = when (feedSettings) { - is AllCommunitiesTopNavPerRelayFilterSet -> filterClassifiedsByAllCommunities(feedSettings, since) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByFollows(feedSettings, since) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since) - is GlobalTopNavPerRelayFilterSet -> filterClassifiedsGlobal(feedSettings, since) - is HashtagTopNavPerRelayFilterSet -> filterClassifiedsByHashtag(feedSettings, since) - is LocationTopNavPerRelayFilterSet -> filterClassifiedsByGeohash(feedSettings, since) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since) - is SingleCommunityTopNavPerRelayFilterSet -> filterClassifiedsByCommunity(feedSettings, since) + is AllCommunitiesTopNavPerRelayFilterSet -> filterClassifiedsByAllCommunities(feedSettings, since, defaultSince) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByFollows(feedSettings, since, defaultSince) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since, defaultSince) + is GlobalTopNavPerRelayFilterSet -> filterClassifiedsGlobal(feedSettings, since, defaultSince) + is HashtagTopNavPerRelayFilterSet -> filterClassifiedsByHashtag(feedSettings, since, defaultSince) + is LocationTopNavPerRelayFilterSet -> filterClassifiedsByGeohash(feedSettings, since, defaultSince) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since, defaultSince) + is SingleCommunityTopNavPerRelayFilterSet -> filterClassifiedsByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt index 67d713ec33..e1d6664ef2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAllCommunities.kt @@ -68,6 +68,7 @@ fun filterClassifiedsAllCommunities( fun filterClassifiedsByAllCommunities( communitySet: AllCommunitiesTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -76,7 +77,7 @@ fun filterClassifiedsByAllCommunities( filterClassifiedsAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt index a1012c980b..034852524e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt @@ -52,6 +52,7 @@ fun filterClassifiedsAuthors( fun filterClassifiedsByAuthors( authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -63,7 +64,7 @@ fun filterClassifiedsByAuthors( filterClassifiedsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() @@ -72,6 +73,7 @@ fun filterClassifiedsByAuthors( fun filterClassifiedsByAuthors( authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -83,7 +85,7 @@ fun filterClassifiedsByAuthors( filterClassifiedsAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt index 5645e2c466..cffe1a0042 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByCommunity.kt @@ -70,6 +70,7 @@ fun filterClassifiedsByCommunity( fun filterClassifiedsByCommunity( communitySet: SingleCommunityTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -79,7 +80,7 @@ fun filterClassifiedsByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt index 4d84d570df..b3d8c2554f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt @@ -27,11 +27,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterClassifiedsByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (followsSet.set.isEmpty()) return emptyList() return followsSet.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince val relay = it.key listOfNotNull( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt index 2d1125f7b8..0c89c881b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByGeohash.kt @@ -53,6 +53,7 @@ fun filterClassifiedsByGeohash( fun filterClassifiedsByGeohash( geoSet: LocationTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (geoSet.set.isEmpty()) return emptyList() @@ -64,7 +65,7 @@ fun filterClassifiedsByGeohash( filterClassifiedsByGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt index bb30a6f333..9645bef0d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByHashtag.kt @@ -54,6 +54,7 @@ fun filterClassifiedsByHashtag( fun filterClassifiedsByHashtag( hashSet: HashtagTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (hashSet.set.isEmpty()) return emptyList() @@ -65,7 +66,7 @@ fun filterClassifiedsByHashtag( filterClassifiedsByHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt index 41ce6ecd13..993e2a118e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt @@ -29,11 +29,12 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent fun filterClassifiedsGlobal( relays: GlobalTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (relays.set.isEmpty()) return emptyList() return relays.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince listOf( RelayBasedFilter( relay = it.key, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt index 43d1cd7704..e3c7c339e9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssembler.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomeOutboxEventsEoseManager import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import kotlinx.coroutines.CoroutineScope @@ -29,6 +30,7 @@ import kotlinx.coroutines.CoroutineScope // This allows multiple screen to be listening to tags, even the same tag class HomeQueryState( val account: Account, + val feedState: AccountFeedContentStates, val scope: CoroutineScope, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt index c264bb8a03..34c15b5705 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/HomeFilterAssemblerSubscription.kt @@ -43,7 +43,11 @@ fun HomeFilterAssemblerSubscription( // even if they are tracking the same tag. val state = remember(accountViewModel.account) { - HomeQueryState(accountViewModel.account, accountViewModel.viewModelScope) + HomeQueryState( + accountViewModel.account, + accountViewModel.feedStates, + accountViewModel.viewModelScope, + ) } KeyDataSourceSubscription(state, dataSource) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt index 2238c324f1..95a28c771d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt @@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import kotlin.collections.flatten val HomePostsByGeohashKinds = listOf( @@ -63,7 +64,7 @@ fun filterHomePostsByGeohashes( Filter( kinds = HomePostsByGeohashKinds, tags = mapOf("g" to geotags.sorted()), - limit = geotags.size * 20, + limit = 100, since = since, ), ), @@ -73,6 +74,7 @@ fun filterHomePostsByGeohashes( fun filterHomePostsByGeohashes( geoSet: LocationTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long?, ): List { if (geoSet.set.isEmpty()) return emptyList() @@ -84,12 +86,12 @@ fun filterHomePostsByGeohashes( filterHomePostsByGeohashes( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) + filterHomePostsByScopes( relay = it.key, scopesToLoad = it.value.geotagScopes, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt index 320c4cd655..a590028396 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGlobal.kt @@ -22,50 +22,16 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent -import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent -import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomePostsConversationKinds +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows.HomePostsNewThreadKinds import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent -import com.vitorpamplona.quartz.nip18Reposts.RepostEvent -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip51Lists.PinListEvent -import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent -import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent -import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent -import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent - -val HomePostsByGlobalKinds = - listOf( - TextNoteEvent.KIND, - RepostEvent.KIND, - GenericRepostEvent.KIND, - ClassifiedsEvent.KIND, - LongTextNoteEvent.KIND, - EphemeralChatEvent.KIND, - HighlightEvent.KIND, - ) - -val HomePostsByGlobalKinds2 = - listOf( - PollNoteEvent.KIND, - AudioTrackEvent.KIND, - AudioHeaderEvent.KIND, - PinListEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - LiveActivitiesChatMessageEvent.KIND, - LiveActivitiesEvent.KIND, - WikiNoteEvent.KIND, - ) fun filterHomePostsByGlobal( relays: GlobalTopNavPerRelayFilterSet, since: SincePerRelayMap?, + newThreadSince: Long?, + repliesSince: Long?, ): List { if (relays.set.isEmpty()) return emptyList() @@ -77,18 +43,18 @@ fun filterHomePostsByGlobal( relay = relayUrl, filter = Filter( - kinds = HomePostsByGlobalKinds, + kinds = HomePostsNewThreadKinds, limit = 50, - since = since, + since = since ?: newThreadSince, ), ), RelayBasedFilter( relay = relayUrl, filter = Filter( - kinds = HomePostsByGlobalKinds2, + kinds = HomePostsConversationKinds, limit = 50, - since = since, + since = since ?: repliesSince, ), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt index 5ee8386b3d..b94679fb48 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt @@ -78,6 +78,7 @@ fun filterHomePostsByHashtags( fun filterHomePostsByHashtags( hashtagSet: HashtagTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long?, ): List { if (hashtagSet.set.isEmpty()) return emptyList() @@ -86,8 +87,7 @@ fun filterHomePostsByHashtags( if (it.value.hashtags.isEmpty()) { null } else { - val since = since?.get(it.key)?.time - + val since = since?.get(it.key)?.time ?: defaultSince return filterHomePostsByHashtags( relay = it.key, hashToLoad = it.value.hashtags, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt index c4ce575b19..04172ee0db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip22Comments/FilterPostsByScopes.kt @@ -41,7 +41,7 @@ fun filterHomePostsByScopes( Filter( kinds = CommentKinds, tags = mapOf("I" to scopesToLoad.toList()), - limit = scopesToLoad.size * 20, + limit = 100, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt index 03d158a668..6bd3ceb1ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt @@ -31,6 +31,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterHomePostsByAllFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + sinceBoundaryNew: Long?, + sinceBoundaryReply: Long?, ): List { if (followsSet.set.isEmpty()) return emptyList() @@ -39,22 +41,25 @@ fun filterHomePostsByAllFollows( listOfNotNull( filter.authors?.let { - filterHomePostsByAuthors(relay, it, since) + filterNewHomePostsByAuthors(relay, it, since ?: sinceBoundaryNew) + }, + filter.authors?.let { + filterReplyHomePostsByAuthors(relay, it, since ?: sinceBoundaryReply) }, filter.geotags?.let { - filterHomePostsByGeohashes(relay, it, since) + filterHomePostsByGeohashes(relay, it, since ?: sinceBoundaryNew) }, filter.geotagScopes?.let { - filterHomePostsByScopes(relay, it, since) + filterHomePostsByScopes(relay, it, since ?: sinceBoundaryNew) }, filter.hashtags?.let { - filterHomePostsByHashtags(relay, it, since) + filterHomePostsByHashtags(relay, it, since ?: sinceBoundaryNew) }, filter.hashtagScopes?.let { - filterHomePostsByScopes(relay, it, since) + filterHomePostsByScopes(relay, it, since ?: sinceBoundaryNew) }, filter.communities?.let { - filterHomePostsFromAllCommunities(relay, it, since) + filterHomePostsFromAllCommunities(relay, it, since ?: sinceBoundaryNew) }, ).flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt index bf6595e0d0..6a63ca2ca7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -23,8 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follo import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap -import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent -import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent @@ -35,38 +33,37 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent -import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import kotlin.math.max -val HomePostsKinds = +val HomePostsNewThreadKinds = listOf( TextNoteEvent.KIND, RepostEvent.KIND, GenericRepostEvent.KIND, ClassifiedsEvent.KIND, LongTextNoteEvent.KIND, - EphemeralChatEvent.KIND, HighlightEvent.KIND, - ) - -val HomePostsKinds2 = - listOf( - PollNoteEvent.KIND, - AudioTrackEvent.KIND, - AudioHeaderEvent.KIND, - PinListEvent.KIND, - InteractiveStoryPrologueEvent.KIND, - LiveActivitiesChatMessageEvent.KIND, - LiveActivitiesEvent.KIND, WikiNoteEvent.KIND, + PollNoteEvent.KIND, + InteractiveStoryPrologueEvent.KIND, ) -fun filterHomePostsByAuthors( +val HomePostsConversationKinds = + listOf( + LiveActivitiesChatMessageEvent.KIND, + CommentEvent.KIND, + LiveActivitiesEvent.KIND, + EphemeralChatEvent.KIND, + ) + +fun filterNewHomePostsByAuthors( relay: NormalizedRelayUrl, authors: Set, since: Long? = null, @@ -78,19 +75,30 @@ fun filterHomePostsByAuthors( relay = relay, filter = Filter( - kinds = HomePostsKinds, + kinds = HomePostsNewThreadKinds, authors = authorList, - limit = authorList.size * 10, + limit = max(authorList.size * 10, 300), since = since, ), ), + ) +} + +fun filterReplyHomePostsByAuthors( + relay: NormalizedRelayUrl, + authors: Set, + since: Long? = null, +): List { + val authorList = authors.sorted() + + return listOf( RelayBasedFilter( relay = relay, filter = Filter( - kinds = HomePostsKinds2, + kinds = HomePostsConversationKinds, authors = authorList, - limit = authorList.size * 10, + limit = max(authorList.size * 10, 300), since = since, ), ), @@ -100,6 +108,8 @@ fun filterHomePostsByAuthors( fun filterHomePostsByAuthors( authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + sinceBoundaryNew: Long?, + sinceBoundaryReply: Long?, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -108,11 +118,16 @@ fun filterHomePostsByAuthors( if (it.value.authors.isEmpty()) { null } else { - filterHomePostsByAuthors( + filterNewHomePostsByAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, - ) + since = since?.get(it.key)?.time ?: sinceBoundaryNew, + ) + + filterReplyHomePostsByAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: sinceBoundaryReply, + ) } }.flatten() } @@ -120,6 +135,8 @@ fun filterHomePostsByAuthors( fun filterHomePostsByAuthors( authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + sinceBoundaryNew: Long?, + sinceBoundaryReply: Long?, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -128,11 +145,16 @@ fun filterHomePostsByAuthors( if (it.value.authors.isEmpty()) { null } else { - filterHomePostsByAuthors( + filterNewHomePostsByAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, - ) + since = since?.get(it.key)?.time ?: sinceBoundaryNew, + ) + + filterReplyHomePostsByAuthors( + relay = it.key, + authors = it.value.authors, + since = since?.get(it.key)?.time ?: sinceBoundaryReply, + ) } }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt index 9d91716445..c04031c3f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch class HomeOutboxEventsEoseManager( @@ -55,15 +56,17 @@ class HomeOutboxEventsEoseManager( since: SincePerRelayMap?, ): List? { val feedSettings = key.followsPerRelay() + val newThreadSince = key.feedState.homeNewThreads.lastNoteCreatedAtIfFilled() + val repliesSince = key.feedState.homeReplies.lastNoteCreatedAtIfFilled() return when (feedSettings) { - is AllCommunitiesTopNavPerRelayFilterSet -> filterHomePostsByAllCommunities(feedSettings, since) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAllFollows(feedSettings, since) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAuthors(feedSettings, since) - is GlobalTopNavPerRelayFilterSet -> filterHomePostsByGlobal(feedSettings, since) - is HashtagTopNavPerRelayFilterSet -> filterHomePostsByHashtags(feedSettings, since) - is LocationTopNavPerRelayFilterSet -> filterHomePostsByGeohashes(feedSettings, since) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAuthors(feedSettings, since) - is SingleCommunityTopNavPerRelayFilterSet -> filterHomePostsByCommunity(feedSettings, since) + is AllCommunitiesTopNavPerRelayFilterSet -> filterHomePostsByAllCommunities(feedSettings, since, newThreadSince) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAllFollows(feedSettings, since, newThreadSince, repliesSince) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAuthors(feedSettings, since, newThreadSince, repliesSince) + is GlobalTopNavPerRelayFilterSet -> filterHomePostsByGlobal(feedSettings, since, newThreadSince, repliesSince) + is HashtagTopNavPerRelayFilterSet -> filterHomePostsByHashtags(feedSettings, since, newThreadSince) + is LocationTopNavPerRelayFilterSet -> filterHomePostsByGeohashes(feedSettings, since, newThreadSince) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAuthors(feedSettings, since, newThreadSince, repliesSince) + is SingleCommunityTopNavPerRelayFilterSet -> filterHomePostsByCommunity(feedSettings, since, newThreadSince) else -> emptyList() } } @@ -94,7 +97,17 @@ class HomeOutboxEventsEoseManager( } }, key.scope.launch(Dispatchers.Default) { - key.followRelayFlow().collectLatest { + key.followRelayFlow().sample(1000).collectLatest { + invalidateFilters() + } + }, + key.account.scope.launch(Dispatchers.Default) { + key.feedState.homeNewThreads.lastNoteCreatedAtWhenFullyLoaded.sample(1000).collectLatest { + invalidateFilters() + } + }, + key.account.scope.launch(Dispatchers.Default) { + key.feedState.homeReplies.lastNoteCreatedAtWhenFullyLoaded.sample(1000).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt index 1fb065d67e..98fc5cc52f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsByAllCommunities.kt @@ -67,6 +67,7 @@ fun filterHomePostsFromAllCommunities( fun filterHomePostsByAllCommunities( communitySet: AllCommunitiesTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -75,7 +76,7 @@ fun filterHomePostsByAllCommunities( filterHomePostsFromAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt index 245b047672..a914febf50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt @@ -100,6 +100,7 @@ fun filterHomePostsFromCommunity( fun filterHomePostsByCommunity( communitySet: SingleCommunityTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -109,7 +110,7 @@ fun filterHomePostsByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index 0af6169f12..95df60a549 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -76,6 +76,8 @@ class CardFeedContentState( override val isRefreshing: MutableState = mutableStateOf(false) + val lastNoteCreatedAtWhenFullyLoaded = MutableStateFlow(null) + fun sendToTop() { if (scrolltoTopPending) return @@ -90,6 +92,8 @@ class CardFeedContentState( private var lastAccount: Account? = null private var lastNotes: Set? = null + fun lastNoteCreatedAtIfFilled() = lastNoteCreatedAtWhenFullyLoaded.value + fun refresh() { viewModelScope.launch(Dispatchers.Default) { refreshSuspended() } } @@ -300,6 +304,10 @@ class CardFeedContentState( } private fun updateFeed(notes: ImmutableList) { + if (notes.size >= localFilter.limit()) { + lastNoteCreatedAtWhenFullyLoaded.tryEmit(notes.lastOrNull()?.createdAt()) + } + val currentState = _feedContent.value if (notes.isEmpty()) { _feedContent.tryEmit(CardFeedState.Empty) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt index c95b286abb..ea2fb6d629 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent +import com.vitorpamplona.quartz.nip68Picture.PictureMeta import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent import com.vitorpamplona.quartz.nip71Video.VideoMeta import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent @@ -44,6 +45,8 @@ class VideoFeedFilter( ) : AdditiveFeedFilter() { override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultStoriesFollowList.value + override fun limit() = 300 + override fun showHiddenKey(): Boolean = account.settings.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || account.settings.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) @@ -82,16 +85,23 @@ class VideoFeedFilter( return urls.isNotEmpty() && (urls.any { RichTextParser.Companion.isImageOrVideoUrl(it) } || isSupportedMimeType) } - fun acceptableiMetas(iMetas: List): Boolean = + fun acceptableVideoiMetas(iMetas: List): Boolean = iMetas.any { - !it.url.contains("youtu.be") && !it.url.contains("youtube.com") && (RichTextParser.Companion.isImageOrVideoUrl(it.url) || (it.mimeType == null || SUPPORTED_VIDEO_FEED_MIME_TYPES_SET.contains(it.mimeType))) + !it.url.contains("youtu.be") && !it.url.contains("youtube.com") && (RichTextParser.isImageOrVideoUrl(it.url) || (it.mimeType == null || SUPPORTED_VIDEO_FEED_MIME_TYPES_SET.contains(it.mimeType))) } + fun acceptablePictureiMetas(iMetas: List): Boolean = + iMetas.any { + !it.url.contains("youtu.be") && !it.url.contains("youtube.com") && (RichTextParser.isImageOrVideoUrl(it.url) || (it.mimeType == null || SUPPORTED_VIDEO_FEED_MIME_TYPES_SET.contains(it.mimeType))) + } + + fun acceptanceEvent(noteEvent: PictureEvent) = acceptablePictureiMetas(noteEvent.imetaTags()) + fun acceptanceEvent(noteEvent: FileHeaderEvent) = acceptableUrls(noteEvent.urls(), noteEvent.mimeType()) - fun acceptanceEvent(noteEvent: VideoVerticalEvent) = acceptableiMetas(noteEvent.imetaTags()) + fun acceptanceEvent(noteEvent: VideoVerticalEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) - fun acceptanceEvent(noteEvent: VideoHorizontalEvent) = acceptableiMetas(noteEvent.imetaTags()) + fun acceptanceEvent(noteEvent: VideoHorizontalEvent) = acceptableVideoiMetas(noteEvent.imetaTags()) fun acceptableEvent( note: Note, @@ -113,7 +123,8 @@ class VideoFeedFilter( SUPPORTED_VIDEO_FEED_MIME_TYPES_SET, ) ) || - noteEvent is PictureEvent + noteEvent is PictureEvent && + acceptanceEvent(noteEvent) ) && params.match(noteEvent) && (params.isHiddenList || account.isAcceptable(note)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt index 99a3d8f21a..11fba18b78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssembler.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.VideoOutboxEventsFilterSubAssembler import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import kotlinx.coroutines.CoroutineScope @@ -29,6 +30,7 @@ import kotlinx.coroutines.CoroutineScope // This allows multiple screen to be listening to tags, even the same tag class VideoQueryState( val account: Account, + val feedState: AccountFeedContentStates, val scope: CoroutineScope, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt index 004a72639e..691cb8dbbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/VideoFilterAssemblerSubscription.kt @@ -43,7 +43,7 @@ fun VideoFilterAssemblerSubscription( // even if they are tracking the same tag. val state = remember(accountViewModel.account) { - VideoQueryState(accountViewModel.account, accountViewModel.viewModelScope) + VideoQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope) } KeyDataSourceSubscription(state, filterAssembler) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt index 26bb84f559..f169dddf01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt @@ -58,15 +58,16 @@ class VideoOutboxEventsFilterSubAssembler( since: SincePerRelayMap?, ): List? { val feedSettings = key.followsPerRelay() + val defaultSince = key.feedState.videoFeed.lastNoteCreatedAtWhenFullyLoaded.value return when (feedSettings) { - is AllCommunitiesTopNavPerRelayFilterSet -> filterPictureAndVideoByAllCommunities(feedSettings, since) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByFollows(feedSettings, since) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since) - is GlobalTopNavPerRelayFilterSet -> filterPictureAndVideoGlobal(feedSettings, since) - is HashtagTopNavPerRelayFilterSet -> filterPictureAndVideoByHashtag(feedSettings, since) - is LocationTopNavPerRelayFilterSet -> filterPictureAndVideoByGeohash(feedSettings, since) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since) - is SingleCommunityTopNavPerRelayFilterSet -> filterPictureAndVideoByCommunity(feedSettings, since) + is AllCommunitiesTopNavPerRelayFilterSet -> filterPictureAndVideoByAllCommunities(feedSettings, since, defaultSince) + is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByFollows(feedSettings, since, defaultSince) + is AuthorsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since, defaultSince) + is GlobalTopNavPerRelayFilterSet -> filterPictureAndVideoGlobal(feedSettings, since, defaultSince) + is HashtagTopNavPerRelayFilterSet -> filterPictureAndVideoByHashtag(feedSettings, since, defaultSince) + is LocationTopNavPerRelayFilterSet -> filterPictureAndVideoByGeohash(feedSettings, since, defaultSince) + is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since, defaultSince) + is SingleCommunityTopNavPerRelayFilterSet -> filterPictureAndVideoByCommunity(feedSettings, since, defaultSince) else -> emptyList() } } @@ -92,7 +93,12 @@ class VideoOutboxEventsFilterSubAssembler( userJobMap[user] = listOf( key.scope.launch(Dispatchers.Default) { - key.followsPerRelayFlow().sample(500).collectLatest { + key.followsPerRelayFlow().sample(1000).collectLatest { + invalidateFilters() + } + }, + key.scope.launch(Dispatchers.Default) { + key.feedState.videoFeed.lastNoteCreatedAtWhenFullyLoaded.sample(1000).collectLatest { invalidateFilters() } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt index 64ad5d7db6..e8075c4dab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt @@ -43,7 +43,7 @@ fun filterPictureAndVideoGeohash( Filter( kinds = PictureAndVideoKinds, tags = mapOf("g" to geoHashes), - limit = 100, + limit = if (since == null) 200 else null, since = since, ), ), @@ -57,7 +57,7 @@ fun filterPictureAndVideoGeohash( "g" to geoHashes, "m" to LegacyMimeTypes, ), - limit = 100, + limit = if (since == null) 200 else null, since = since, ), ), @@ -67,6 +67,7 @@ fun filterPictureAndVideoGeohash( fun filterPictureAndVideoByGeohash( geoSet: LocationTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (geoSet.set.isEmpty()) return emptyList() @@ -78,7 +79,7 @@ fun filterPictureAndVideoByGeohash( filterPictureAndVideoGeohash( relay = it.key, geotags = it.value.geotags, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt index 3b21de472d..797cb856fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt @@ -44,7 +44,7 @@ fun filterPictureAndVideoHashtag( Filter( kinds = PictureAndVideoKinds, tags = mapOf("t" to hashtags), - limit = 50, + limit = if (since == null) 100 else null, since = since, ), ), @@ -58,7 +58,7 @@ fun filterPictureAndVideoHashtag( "t" to hashtags, "m" to LegacyMimeTypes, ), - limit = 50, + limit = if (since == null) 100 else null, since = since, ), ), @@ -68,6 +68,7 @@ fun filterPictureAndVideoHashtag( fun filterPictureAndVideoByHashtag( hashSet: HashtagTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (hashSet.set.isEmpty()) return emptyList() @@ -79,7 +80,7 @@ fun filterPictureAndVideoByHashtag( filterPictureAndVideoHashtag( relay = relayHashSet.key, hashtags = relayHashSet.value.hashtags, - since = since?.get(relayHashSet.key)?.time, + since = since?.get(relayHashSet.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt index 953a323ee0..2e2295ea5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt @@ -31,18 +31,19 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter fun filterPictureAndVideoGlobal( relays: GlobalTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (relays.set.isEmpty()) return emptyList() return relays.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince listOf( RelayBasedFilter( relay = it.key, filter = Filter( kinds = PictureAndVideoKinds, - limit = 100, + limit = if (since == null) 50 else null, since = since, ), ), @@ -52,7 +53,7 @@ fun filterPictureAndVideoGlobal( Filter( kinds = PictureAndVideoLegacyKinds, tags = LegacyMimeTypeMap, - limit = 100, + limit = if (since == null) 50 else null, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt index ebd5b7d3c1..c1080bd306 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlin.math.max fun filterPictureAndVideoAuthors( relay: NormalizedRelayUrl, @@ -44,7 +45,7 @@ fun filterPictureAndVideoAuthors( Filter( authors = authorList, kinds = PictureAndVideoKinds, - limit = 100, + limit = if (since == null) max(authorList.size * 20, 200) else null, since = since, ), ), @@ -55,7 +56,7 @@ fun filterPictureAndVideoAuthors( authors = authorList, kinds = PictureAndVideoLegacyKinds, tags = LegacyMimeTypeMap, - limit = 100, + limit = if (since == null) max(authorList.size * 20, 200) else null, since = since, ), ), @@ -65,6 +66,7 @@ fun filterPictureAndVideoAuthors( fun filterPictureAndVideoByAuthors( authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -76,7 +78,7 @@ fun filterPictureAndVideoByAuthors( filterPictureAndVideoAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() @@ -85,6 +87,7 @@ fun filterPictureAndVideoByAuthors( fun filterPictureAndVideoByAuthors( authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (authorSet.set.isEmpty()) return emptyList() @@ -96,7 +99,7 @@ fun filterPictureAndVideoByAuthors( filterPictureAndVideoAuthors( relay = it.key, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) } }.flatten() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt index 4264ab6cc1..885a85db74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt @@ -30,25 +30,26 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterPictureAndVideoByFollows( followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (followsSet.set.isEmpty()) return emptyList() return followsSet.set.flatMap { - val since = since?.get(it.key)?.time + val since = since?.get(it.key)?.time ?: defaultSince val relay = it.key listOfNotNull( - it.value.authors?.let { - filterPictureAndVideoAuthors(relay, it, since) + it.value.authors?.let { authors -> + filterPictureAndVideoAuthors(relay, authors, since) }, - it.value.geotags?.let { - filterPictureAndVideoGeohash(relay, it, since) + it.value.geotags?.let { geotags -> + filterPictureAndVideoGeohash(relay, geotags, since) }, - it.value.hashtags?.let { - filterPictureAndVideoHashtag(relay, it, since) + it.value.hashtags?.let { hashtags -> + filterPictureAndVideoHashtag(relay, hashtags, since) }, - it.value.communities?.let { - filterPictureAndVideoAllCommunities(relay, it, since) + it.value.communities?.let { communities -> + filterPictureAndVideoAllCommunities(relay, communities, since) }, ).flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt index 7f55fac630..90d0bafae5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt @@ -52,7 +52,7 @@ fun filterPictureAndVideoAllCommunities( "a" to communityList, "k" to PictureAndVideoKTags, ), - limit = 100, + limit = if (since == null) 200 else null, since = since, ), ), @@ -63,7 +63,7 @@ fun filterPictureAndVideoAllCommunities( Filter( tags = mapOf("a" to communityList), kinds = PictureAndVideoKinds, - limit = 100, + limit = if (since == null) 200 else null, since = since, ), ), @@ -78,7 +78,7 @@ fun filterPictureAndVideoAllCommunities( "a" to communityList, "k" to listOf(FileHeaderEvent.KIND.toString(), FileStorageHeaderEvent.KIND.toString()), ), - limit = 100, + limit = if (since == null) 200 else null, since = since, ), ), @@ -93,7 +93,7 @@ fun filterPictureAndVideoAllCommunities( "m" to LegacyMimeTypes, ), kinds = PictureAndVideoLegacyKinds, - limit = 100, + limit = if (since == null) 200 else null, since = since, ), ), @@ -103,6 +103,7 @@ fun filterPictureAndVideoAllCommunities( fun filterPictureAndVideoByAllCommunities( communitySet: AllCommunitiesTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -111,7 +112,7 @@ fun filterPictureAndVideoByAllCommunities( filterPictureAndVideoAllCommunities( relay = it.key, communities = it.value.communities, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt index 11b0f7c584..a5dc40bcbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByCommunity.kt @@ -52,7 +52,7 @@ fun filterPictureAndVideoCommunity( "a" to listOf(community), "k" to PictureAndVideoKTags, ), - limit = 500, + limit = 200, since = since, ), ), @@ -64,7 +64,7 @@ fun filterPictureAndVideoCommunity( authors = authors, tags = mapOf("a" to listOf(community)), kinds = PictureAndVideoKinds, - limit = 500, + limit = 200, since = since, ), ), @@ -80,7 +80,7 @@ fun filterPictureAndVideoCommunity( "a" to listOf(community), "k" to PictureAndVideoLegacyKTags, ), - limit = 500, + limit = 200, since = since, ), ), @@ -96,7 +96,7 @@ fun filterPictureAndVideoCommunity( "m" to LegacyMimeTypes, ), kinds = PictureAndVideoLegacyKinds, - limit = 500, + limit = 200, since = since, ), ), @@ -106,6 +106,7 @@ fun filterPictureAndVideoCommunity( fun filterPictureAndVideoByCommunity( communitySet: SingleCommunityTopNavPerRelayFilterSet, since: SincePerRelayMap?, + defaultSince: Long? = null, ): List { if (communitySet.set.isEmpty()) return emptyList() @@ -115,7 +116,7 @@ fun filterPictureAndVideoByCommunity( relay = it.key, community = it.value.community, authors = it.value.authors, - since = since?.get(it.key)?.time, + since = since?.get(it.key)?.time ?: defaultSince, ) }.flatten() } From 95f463b0aaa85a538999d019d5865e7e65e278ae Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 22 Jul 2025 20:12:43 -0400 Subject: [PATCH 117/185] Showing ephemeral chats in the messages list. Fixing a bug of the last message with unloaded event --- .../amethyst/model/LocalCache.kt | 14 +++--- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 16 +++++++ .../chats/rooms/ChatroomHeaderCompose.kt | 43 +++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 2e26088071..7974342bf9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -322,8 +322,6 @@ object LocalCache : ILocalCache { fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId) - fun getChatroomListIfExists(key: String): ChatroomList? = chatroomList.get(key) - fun getPublicChatChannelIfExists(key: String): PublicChatChannel? = publicChatChannels.get(key) fun getEphemeralChatChannelIfExists(key: RoomId): EphemeralChatChannel? = ephemeralChannels.get(key) @@ -1571,6 +1569,8 @@ object LocalCache : ILocalCache { ): Boolean { val channelId = event.channelId() ?: return false + val new = consumeRegularEvent(event, relay, wasVerified) + val channel = checkGetOrCreatePublicChatChannel(channelId) if (channel == null) { Log.w("LocalCache", "Unable to create public chat channel for event ${event.toJson()}") @@ -1580,7 +1580,7 @@ object LocalCache : ILocalCache { val note = getOrCreateNote(event.id) channel.addNote(note, relay) - return consumeRegularEvent(event, relay, wasVerified) + return new } fun consume( @@ -1590,11 +1590,13 @@ object LocalCache : ILocalCache { ): Boolean { val roomId = event.roomId() ?: return false + val new = consumeRegularEvent(event, relay, wasVerified) + val note = getOrCreateNote(event.id) val channel = getOrCreateEphemeralChannel(roomId) channel.addNote(note, relay) - return consumeRegularEvent(event, relay, wasVerified) + return new } fun consume( @@ -1604,12 +1606,14 @@ object LocalCache : ILocalCache { ): Boolean { val activityAddress = event.activityAddress() ?: return false + val new = consumeRegularEvent(event, relay, wasVerified) + val channel = getOrCreateLiveChannel(activityAddress) val note = getOrCreateNote(event.id) channel.addNote(note, relay) - return consumeRegularEvent(event, relay, wasVerified) + return new } fun consume( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index b1a1ed18b0..a834331c41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -33,12 +33,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers @@ -172,6 +174,20 @@ fun LoadPublicChatChannel( channel.value?.let { content(it) } } +@Composable +fun LoadEphemeralChatChannel( + id: RoomId, + accountViewModel: AccountViewModel, + content: @Composable (EphemeralChatChannel) -> Unit, +) { + val channel = + produceStateIfNotNull(accountViewModel.getEphemeralChatChannelIfExists(id), id) { + value = accountViewModel.checkGetOrCreateEphemeralChatChannel(id) + } + + channel.value?.let { content(it) } +} + @Composable fun LoadLiveActivityChannel( id: Address, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 57f5e6d7ab..8f2e7fe071 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent @@ -72,11 +73,14 @@ import com.vitorpamplona.amethyst.ui.note.ObserveDraftEvent import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header.RoomNameDisplay +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.LoadEphemeralChatChannel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.loadRelayInfo import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.grayText import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent @@ -146,6 +150,13 @@ private fun ChatroomEntry( val room = baseNoteEvent.chatroomKey(accountViewModel.userProfile().pubkeyHex) UserRoomCompose(room, lastMessage, accountViewModel, nav) } + is EphemeralChatEvent -> { + baseNoteEvent.roomId()?.let { + LoadEphemeralChatChannel(it, accountViewModel) { channel -> + ChannelRoomCompose(lastMessage, channel, accountViewModel, nav) + } + } + } else -> BlankNote() } } @@ -191,6 +202,38 @@ private fun ChannelRoomCompose( ) } +@Composable +private fun ChannelRoomCompose( + lastMessage: Note, + channel: EphemeralChatChannel, + accountViewModel: AccountViewModel, + nav: INav, +) { + val authorName by observeUserName(lastMessage.author!!, accountViewModel) + val channelState by observeChannel(channel, accountViewModel) + + val channel = channelState?.channel as? EphemeralChatChannel ?: return + + val relayInfo by loadRelayInfo(channel.roomId.relayUrl, accountViewModel) + + val noteEvent = lastMessage.event + val description = noteEvent?.content?.take(200) + + val lastReadTime by accountViewModel.account.loadLastReadFlow("Channel/${channel.roomId.toKey()}").collectAsStateWithLifecycle() + + ChannelName( + channelIdHex = channel.roomId.toKey(), + channelPicture = relayInfo.icon, + channelTitle = { modifier -> ChannelTitleWithLabelInfo(channel.toBestDisplayName(), R.string.ephemeral_relay_chat, modifier) }, + channelLastTime = lastMessage.createdAt(), + channelLastContent = "$authorName: $description", + hasNewMessages = (noteEvent?.createdAt ?: Long.MIN_VALUE) > lastReadTime, + loadProfilePicture = accountViewModel.settings.showProfilePictures.value, + loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + onClick = { nav.nav(routeFor(channel)) }, + ) +} + @Composable private fun ChannelTitleWithLabelInfo( channelName: String, From 064a732ec80bc7735e1291b3f34b371267e69a7f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 23 Jul 2025 08:44:41 -0400 Subject: [PATCH 118/185] Fixes leaving and joining channels --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/LocalCache.kt | 33 ++++++----- .../model/emphChat/EphemeralChatListState.kt | 1 - .../experimental/ephemChat/chat/RoomId.kt | 2 +- .../ephemChat/list/EphemeralChatListEvent.kt | 12 +++- .../nip28PublicChat/list/ChannelListEvent.kt | 6 +- .../quartz/nip51Lists/TagArrayExt.kt | 13 ++++ .../quartz/nip51Lists/TagArrayExt.kt | 59 +++++++++++++++++++ 8 files changed, 105 insertions(+), 23 deletions(-) create mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 32c66aab6d..46367f35ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -854,8 +854,8 @@ class Account( fun sendMyPublicAndPrivateOutbox(event: Event?) { if (event == null) return - client.send(event, outboxRelays.flow.value) cache.justConsumeMyOwnEvent(event) + client.send(event, outboxRelays.flow.value) } fun sendMyPublicAndPrivateOutbox(events: List) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 7974342bf9..1fab347ad0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -1571,14 +1571,16 @@ object LocalCache : ILocalCache { val new = consumeRegularEvent(event, relay, wasVerified) - val channel = checkGetOrCreatePublicChatChannel(channelId) - if (channel == null) { - Log.w("LocalCache", "Unable to create public chat channel for event ${event.toJson()}") - return false - } + if (new) { + val channel = checkGetOrCreatePublicChatChannel(channelId) + if (channel == null) { + Log.w("LocalCache", "Unable to create public chat channel for event ${event.toJson()}") + return false + } - val note = getOrCreateNote(event.id) - channel.addNote(note, relay) + val note = getOrCreateNote(event.id) + channel.addNote(note, relay) + } return new } @@ -1592,9 +1594,11 @@ object LocalCache : ILocalCache { val new = consumeRegularEvent(event, relay, wasVerified) - val note = getOrCreateNote(event.id) - val channel = getOrCreateEphemeralChannel(roomId) - channel.addNote(note, relay) + if (new) { + val note = getOrCreateNote(event.id) + val channel = getOrCreateEphemeralChannel(roomId) + channel.addNote(note, relay) + } return new } @@ -1608,10 +1612,11 @@ object LocalCache : ILocalCache { val new = consumeRegularEvent(event, relay, wasVerified) - val channel = getOrCreateLiveChannel(activityAddress) - - val note = getOrCreateNote(event.id) - channel.addNote(note, relay) + if (new) { + val channel = getOrCreateLiveChannel(activityAddress) + val note = getOrCreateNote(event.id) + channel.addNote(note, relay) + } return new } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt index b8a5368215..0a9378ebb8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt @@ -97,7 +97,6 @@ class EphemeralChatListState( suspend fun unfollow(channel: EphemeralChatChannel): EphemeralChatListEvent? { val ephemeralChatList = getEphemeralChatList() - return if (ephemeralChatList != null) { EphemeralChatListEvent.remove( earlierVersion = ephemeralChatList, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt index 42cc41a3c7..f4ca23a744 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/chat/RoomId.kt @@ -41,5 +41,5 @@ data class RoomId( } } - fun toTagArray() = RoomIdTag.assemble(id, relayUrl) + fun toTagArray() = RoomIdTag.assemble(this) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt index 37bbaf1bb7..9a9de6d9d5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/ephemChat/list/EphemeralChatListEvent.kt @@ -23,6 +23,8 @@ package com.vitorpamplona.quartz.experimental.ephemChat.list import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.rooms +import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag +import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag.Companion.parse import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder @@ -35,7 +37,7 @@ import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent -import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.nip51Lists.removeParsing import com.vitorpamplona.quartz.utils.TimeUtils import java.lang.reflect.Modifier.isPrivate @@ -48,6 +50,10 @@ class EphemeralChatListEvent( content: String, sig: HexKey, ) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicRooms() = tags.rooms() + + fun publicRoomSet() = tags.roomSet() + companion object { const val KIND = 10023 const val ALT = "Ephemeral Chat List" @@ -109,8 +115,8 @@ class EphemeralChatListEvent( ): EphemeralChatListEvent { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() return resign( - privateTags = privateTags.remove(room.toTagArray()), - tags = earlierVersion.tags.remove(room.toTagArray()), + privateTags = privateTags.removeParsing(RoomIdTag::parse, room), + tags = earlierVersion.tags.removeParsing(RoomIdTag::parse, room), signer = signer, createdAt = createdAt, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt index 18f3f373e5..49e72ed80d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/list/ChannelListEvent.kt @@ -37,8 +37,8 @@ import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List -import com.vitorpamplona.quartz.nip51Lists.remove import com.vitorpamplona.quartz.nip51Lists.removeAny +import com.vitorpamplona.quartz.nip51Lists.removeParsing import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -142,8 +142,8 @@ class ChannelListEvent( ): ChannelListEvent { val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() return resign( - privateTags = privateTags.remove(channel.toTagArray()), - tags = earlierVersion.tags.remove(channel.toTagArray()), + privateTags = privateTags.removeParsing(ChannelTag::parseId, channel.eventId), + tags = earlierVersion.tags.removeParsing(ChannelTag::parseId, channel.eventId), signer = signer, createdAt = createdAt, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt index e245632147..0b03a2b19f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt @@ -20,9 +20,11 @@ */ package com.vitorpamplona.quartz.nip51Lists +import com.vitorpamplona.quartz.nip01Core.core.Tag import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.utils.startsWith import com.vitorpamplona.quartz.utils.startsWithAny +import kotlin.collections.ArrayList inline fun TagArray.filterToArray(predicate: (Array) -> Boolean): TagArray = filterTo(ArrayList(), predicate).toTypedArray() @@ -30,6 +32,17 @@ inline fun TagArray.remove(predicate: (Array) -> Boolean): TagArray = fi fun TagArray.remove(startsWith: Array): TagArray = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray() +fun TagArray.removeParsing( + transform: (Tag) -> R, + equalsTo: R, +): TagArray = + filterNotTo( + destination = ArrayList(this.size), + predicate = { + transform(it) == equalsTo + }, + ).toTypedArray() + fun TagArray.removeAny(startsWith: List>): TagArray = filterNotTo( ArrayList(this.size), diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt new file mode 100644 index 0000000000..b5c40011e7 --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip51Lists/TagArrayExt.kt @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists + +import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId +import com.vitorpamplona.quartz.experimental.ephemChat.list.tags.RoomIdTag +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import junit.framework.TestCase.assertTrue +import org.junit.Test +import java.util.Arrays + +class TagArrayExt { + val tags = + arrayOf( + arrayOf("group", "test", "wss://nos.lol"), + arrayOf("group", "_", "wss://nos.lol"), + ) + + val expectedTags = + arrayOf( + arrayOf("group", "test", "wss://nos.lol"), + ) + + @Test + fun testRemove() { + assertTrue( + Arrays.deepEquals(expectedTags, tags.remove(arrayOf("group", "_", "wss://nos.lol"))), + ) + } + + @Test + fun testRemoveParsing() { + val removing = RoomId("_", RelayUrlNormalizer.normalize("wss://nos.lol")) + assertTrue( + Arrays.deepEquals( + expectedTags, + tags.removeParsing(RoomIdTag::parse, removing), + ), + ) + } +} From c221506e7836d5e918b6625c7f6bec4852bf309c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 23 Jul 2025 08:50:14 -0400 Subject: [PATCH 119/185] Fixes square loading image on Follow packs --- .../vitorpamplona/amethyst/ui/components/MyAsyncImage.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt index 9effe29c4e..4e847ec37f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt @@ -76,13 +76,13 @@ fun MyAsyncImage( } } else { WaitAndDisplay { - Box(contentAlignment = Alignment.Center) { - if (onLoadingBackground != null) { + if (onLoadingBackground != null) { + Box(loadedImageModifier, contentAlignment = Alignment.Center) { onLoadingBackground() LoadingAnimation(Size40dp, Size6dp) - } else { - DisplayUrlWithLoadingSymbol(imageUrl) } + } else { + DisplayUrlWithLoadingSymbol(imageUrl) } } } From 3a0d7ddac164a12061fa6cef9cceb487065495af Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 23 Jul 2025 09:26:07 -0400 Subject: [PATCH 120/185] Removes unnecessary spec in Channel --- .../src/main/java/com/vitorpamplona/amethyst/model/Channel.kt | 2 -- .../amethyst/model/emphChat/EphemeralChatChannel.kt | 2 +- .../amethyst/model/nip28PublicChats/PublicChatChannel.kt | 2 +- .../amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index df0ac0bac8..9ad0c49665 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -85,8 +85,6 @@ abstract class Channel { notes.remove(note.idHex) } - abstract fun anyNameStartsWith(prefix: String): Boolean - fun pruneOldMessages(): Set { val important = notes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt index ac9ad74a4c..12a28673d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt @@ -32,5 +32,5 @@ class EphemeralChatChannel( override fun toBestDisplayName() = roomId.toDisplayKey() - override fun anyNameStartsWith(prefix: String): Boolean = roomId.id.contains(prefix, true) + fun anyNameStartsWith(prefix: String): Boolean = roomId.id.contains(prefix, true) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt index 0018d3b729..e7ea766ebe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt @@ -94,7 +94,7 @@ class PublicChatChannel( return info.picture ?: creator?.info?.banner } - override fun anyNameStartsWith(prefix: String): Boolean = + fun anyNameStartsWith(prefix: String): Boolean = idHex.startsWith(prefix) || info.name?.contains(prefix, true) == true || info.about?.contains(prefix, true) == true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt index cf05a495e1..f846613e96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -61,7 +61,7 @@ class LiveActivitiesChannel( fun profilePicture(): String? = info?.image()?.ifBlank { null } - override fun anyNameStartsWith(prefix: String): Boolean = + fun anyNameStartsWith(prefix: String): Boolean = info?.title()?.contains(prefix, true) == true || info?.summary()?.contains(prefix, true) == true From a7dbd787f4f5ab4a6cfeb2e33e3c8c0b3681f9b2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 23 Jul 2025 10:50:51 -0400 Subject: [PATCH 121/185] Creates a garbage collector-friendly note cache. --- .../amethyst/model/LocalCache.kt | 13 +- .../amethyst/model/MediaAspectRatioCache.kt | 4 +- .../eventCache/MemoryTrimmingService.kt | 1 + .../vitorpamplona/quartz/utils/LargeCache.kt | 642 +++++++++--------- .../quartz/utils/LargeSoftCache.kt | 627 +++++++++++++++++ 5 files changed, 961 insertions(+), 326 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeSoftCache.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1fab347ad0..a5504a4a3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -179,6 +179,7 @@ import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.LargeCache +import com.vitorpamplona.quartz.utils.LargeSoftCache import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -208,9 +209,9 @@ interface ILocalCache { object LocalCache : ILocalCache { val antiSpam = AntiSpamFilter() - val users = LargeCache() - val notes = LargeCache() - val addressables = LargeCache() + val users = LargeSoftCache() + val notes = LargeSoftCache() + val addressables = LargeSoftCache() val chatroomList = LargeCache() val publicChatChannels = LargeCache() @@ -2163,6 +2164,12 @@ object LocalCache : ILocalCache { return newNotes } + fun cleanMemory() { + notes.cleanUp() + addressables.cleanUp() + users.cleanUp() + } + fun cleanObservers() { notes.forEach { _, it -> it.clearFlow() } addressables.forEach { _, it -> it.clearFlow() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt index dcf2401ef8..b4b199f9dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/MediaAspectRatioCache.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.model import android.util.LruCache interface MutableMediaAspectRatioCache { - fun get(url: String): Float + fun get(url: String): Float? fun add( url: String, @@ -35,7 +35,7 @@ interface MutableMediaAspectRatioCache { object MediaAspectRatioCache : MutableMediaAspectRatioCache { val mediaAspectRatioCacheByUrl = LruCache(1000) - override fun get(url: String) = mediaAspectRatioCacheByUrl.get(url) + override fun get(url: String): Float? = mediaAspectRatioCacheByUrl.get(url) override fun add( url: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt index 1006244a5d..6d31d5578e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/eventCache/MemoryTrimmingService.kt @@ -36,6 +36,7 @@ class MemoryTrimmingService( account: Account? = null, otherAccounts: List, ) { + cache.cleanMemory() cache.cleanObservers() account?.let { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt index a1bd268bc0..bd61c05d19 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeCache.kt @@ -170,25 +170,25 @@ class LargeCache { return runner.count } - public fun associate(transform: (K, V) -> Pair): Map { + fun associate(transform: (K, V) -> Pair): Map { val runner = BiAssociateCollector(size(), transform) innerForEach(runner) return runner.results } - public fun associateNotNull(transform: (K, V) -> Pair?): Map { + fun associateNotNull(transform: (K, V) -> Pair?): Map { val runner = BiAssociateNotNullCollector(size(), transform) innerForEach(runner) return runner.results } - public fun associateWith(transform: (K, V) -> U?): Map { + fun associateWith(transform: (K, V) -> U?): Map { val runner = BiAssociateWithCollector(size(), transform) innerForEach(runner) return runner.results } - public fun associateNotNullWith(transform: (K, V) -> U): Map { + fun associateNotNullWith(transform: (K, V) -> U): Map { val runner = BiAssociateNotNullWithCollector(size(), transform) innerForEach(runner) return runner.results @@ -227,338 +227,338 @@ class LargeCache { buffer.append(postfix) return buffer.toString() } -} -fun interface BiFilter { - fun filter( - k: K, - v: V, - ): Boolean -} - -class BiFilterCollector( - val filter: BiFilter, -) : BiConsumer { - var results: ArrayList = ArrayList() - - override fun accept( - k: K, - v: V, - ) { - if (filter.filter(k, v)) { - results.add(v) - } + fun interface BiFilter { + fun filter( + k: K, + v: V, + ): Boolean } -} -class BiFilterUniqueCollector( - val filter: BiFilter, -) : BiConsumer { - var results: HashSet = HashSet() + class BiFilterCollector( + val filter: BiFilter, + ) : BiConsumer { + var results: ArrayList = ArrayList() - override fun accept( - k: K, - v: V, - ) { - if (filter.filter(k, v)) { - results.add(v) - } - } -} - -fun interface BiMapper { - fun map( - k: K, - v: V, - ): R? -} - -fun interface BiMapperNotNull { - fun map( - k: K, - v: V, - ): R -} - -class BiMapCollector( - val mapper: BiMapper, -) : BiConsumer { - var results: ArrayList = ArrayList() - - override fun accept( - k: K, - v: V, - ) { - val result = mapper.map(k, v) - if (result != null) { - results.add(result) - } - } -} - -class BiAssociateCollector( - val size: Int, - val mapper: BiMapperNotNull>, -) : BiConsumer { - var results: LinkedHashMap = LinkedHashMap(size) - - override fun accept( - k: K, - v: V, - ) { - val pair = mapper.map(k, v) - results.put(pair.first, pair.second) - } -} - -class BiAssociateNotNullCollector( - val size: Int, - val mapper: BiMapper?>, -) : BiConsumer { - var results: LinkedHashMap = LinkedHashMap(size) - - override fun accept( - k: K, - v: V, - ) { - val pair = mapper.map(k, v) - if (pair != null) { - results.put(pair.first, pair.second) - } - } -} - -class BiAssociateWithCollector( - val size: Int, - val mapper: BiMapper, -) : BiConsumer { - var results: LinkedHashMap = LinkedHashMap(size) - - override fun accept( - k: K, - v: V, - ) { - results.put(k, mapper.map(k, v)) - } -} - -class BiAssociateNotNullWithCollector( - val size: Int, - val mapper: BiMapper, -) : BiConsumer { - var results: LinkedHashMap = LinkedHashMap(size) - - override fun accept( - k: K, - v: V, - ) { - val newValue = mapper.map(k, v) - if (newValue != null) { - results.put(k, newValue) - } - } -} - -class BiMapUniqueCollector( - val mapper: BiMapper, -) : BiConsumer { - var results: HashSet = HashSet() - - override fun accept( - k: K, - v: V, - ) { - val result = mapper.map(k, v) - if (result != null) { - results.add(result) - } - } -} - -class BiMapFlattenCollector( - val mapper: BiMapper?>, -) : BiConsumer { - var results: ArrayList = ArrayList() - - override fun accept( - k: K, - v: V, - ) { - val result = mapper.map(k, v) - if (result != null) { - results.addAll(result) - } - } -} - -class BiMapFlattenUniqueCollector( - val mapper: BiMapper?>, -) : BiConsumer { - var results: HashSet = HashSet() - - override fun accept( - k: K, - v: V, - ) { - val result = mapper.map(k, v) - if (result != null) { - results.addAll(result) - } - } -} - -fun interface BiNotNullMapper { - fun map( - k: K, - v: V, - ): R -} - -class BiNotNullMapCollector( - val mapper: BiNotNullMapper, -) : BiConsumer { - var results: ArrayList = ArrayList() - - override fun accept( - k: K, - v: V, - ) { - results.add(mapper.map(k, v)) - } -} - -fun interface BiSumOf { - fun map( - k: K, - v: V, - ): Int -} - -class BiMaxOfCollector( - val filter: BiFilter, - val comparator: Comparator, -) : BiConsumer { - var maxK: K? = null - var maxV: V? = null - - override fun accept( - k: K, - v: V, - ) { - if (filter.filter(k, v)) { - if (maxK == null || comparator.compare(v, maxV) > 0) { - maxK = k - maxV = v + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + results.add(v) } } } -} -class BiSumOfCollector( - val mapper: BiSumOf, -) : BiConsumer { - var sum = 0 + class BiFilterUniqueCollector( + val filter: BiFilter, + ) : BiConsumer { + var results: HashSet = HashSet() - override fun accept( - k: K, - v: V, - ) { - sum += mapper.map(k, v) + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + results.add(v) + } + } } -} -fun interface BiSumOfLong { - fun map( - k: K, - v: V, - ): Long -} - -class BiSumOfLongCollector( - val mapper: BiSumOfLong, -) : BiConsumer { - var sum = 0L - - override fun accept( - k: K, - v: V, - ) { - sum += mapper.map(k, v) + fun interface BiMapper { + fun map( + k: K, + v: V, + ): R? } -} -class BiGroupByCollector( - val mapper: BiNotNullMapper, -) : BiConsumer { - var results = HashMap>() + fun interface BiMapperNotNull { + fun map( + k: K, + v: V, + ): R + } - override fun accept( - k: K, - v: V, - ) { - val group = mapper.map(k, v) + class BiMapCollector( + val mapper: BiMapper, + ) : BiConsumer { + var results: ArrayList = ArrayList() - val list = results[group] - if (list == null) { - val answer = ArrayList() - answer.add(v) - results[group] = answer - } else { - list.add(v) + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.add(result) + } + } + } + + class BiAssociateCollector( + val size: Int, + val mapper: BiMapperNotNull>, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val pair = mapper.map(k, v) + results.put(pair.first, pair.second) + } + } + + class BiAssociateNotNullCollector( + val size: Int, + val mapper: BiMapper?>, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val pair = mapper.map(k, v) + if (pair != null) { + results.put(pair.first, pair.second) + } + } + } + + class BiAssociateWithCollector( + val size: Int, + val mapper: BiMapper, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + results.put(k, mapper.map(k, v)) + } + } + + class BiAssociateNotNullWithCollector( + val size: Int, + val mapper: BiMapper, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val newValue = mapper.map(k, v) + if (newValue != null) { + results.put(k, newValue) + } + } + } + + class BiMapUniqueCollector( + val mapper: BiMapper, + ) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.add(result) + } + } + } + + class BiMapFlattenCollector( + val mapper: BiMapper?>, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.addAll(result) + } + } + } + + class BiMapFlattenUniqueCollector( + val mapper: BiMapper?>, + ) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.addAll(result) + } + } + } + + fun interface BiNotNullMapper { + fun map( + k: K, + v: V, + ): R + } + + class BiNotNullMapCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + results.add(mapper.map(k, v)) + } + } + + fun interface BiSumOf { + fun map( + k: K, + v: V, + ): Int + } + + class BiMaxOfCollector( + val filter: BiFilter, + val comparator: Comparator, + ) : BiConsumer { + var maxK: K? = null + var maxV: V? = null + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + if (maxK == null || comparator.compare(v, maxV) > 0) { + maxK = k + maxV = v + } + } + } + } + + class BiSumOfCollector( + val mapper: BiSumOf, + ) : BiConsumer { + var sum = 0 + + override fun accept( + k: K, + v: V, + ) { + sum += mapper.map(k, v) + } + } + + fun interface BiSumOfLong { + fun map( + k: K, + v: V, + ): Long + } + + class BiSumOfLongCollector( + val mapper: BiSumOfLong, + ) : BiConsumer { + var sum = 0L + + override fun accept( + k: K, + v: V, + ) { + sum += mapper.map(k, v) + } + } + + class BiGroupByCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap>() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val list = results[group] + if (list == null) { + val answer = ArrayList() + answer.add(v) + results[group] = answer + } else { + list.add(v) + } + } + } + + class BiCountByGroupCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val count = results[group] + if (count == null) { + results[group] = 1 + } else { + results[group] = count + 1 + } + } + } + + class BiSumByGroupCollector( + val mapper: BiNotNullMapper, + val sumOf: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val sum = results[group] + if (sum == null) { + results[group] = sumOf.map(k, v) + } else { + results[group] = sum + sumOf.map(k, v) + } + } + } + + class BiCountIfCollector( + val filter: BiFilter, + ) : BiConsumer { + var count = 0 + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) count++ } } } - -class BiCountByGroupCollector( - val mapper: BiNotNullMapper, -) : BiConsumer { - var results = HashMap() - - override fun accept( - k: K, - v: V, - ) { - val group = mapper.map(k, v) - - val count = results[group] - if (count == null) { - results[group] = 1 - } else { - results[group] = count + 1 - } - } -} - -class BiSumByGroupCollector( - val mapper: BiNotNullMapper, - val sumOf: BiNotNullMapper, -) : BiConsumer { - var results = HashMap() - - override fun accept( - k: K, - v: V, - ) { - val group = mapper.map(k, v) - - val sum = results[group] - if (sum == null) { - results[group] = sumOf.map(k, v) - } else { - results[group] = sum + sumOf.map(k, v) - } - } -} - -class BiCountIfCollector( - val filter: BiFilter, -) : BiConsumer { - var count = 0 - - override fun accept( - k: K, - v: V, - ) { - if (filter.filter(k, v)) count++ - } -} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeSoftCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeSoftCache.kt new file mode 100644 index 0000000000..50f539bd69 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/LargeSoftCache.kt @@ -0,0 +1,627 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils + +import java.lang.ref.WeakReference +import java.util.concurrent.ConcurrentSkipListMap +import java.util.function.BiConsumer + +class LargeSoftCache { + private val cache = ConcurrentSkipListMap>() + + fun keys() = cache.keys + + fun get(key: K): V? { + val softRef = cache.get(key) + val value = softRef?.get() + + return if (value != null) { + value + } else { + cache.remove(key) + null + } + } + + fun remove(key: K) = cache.remove(key) + + fun size() = cache.size + + fun isEmpty() = cache.isEmpty() + + fun clear() = cache.clear() + + fun containsKey(key: K) = cache.containsKey(key) + + /** + * Puts an object into the cache with a specified key. + * The object is stored as a SoftReference. + * + * @param key The key to associate with the object. + * @param value The object to cache. + */ + fun put( + key: K, + value: V, + ) { + cache.put(key, WeakReference(value)) + } + + /** + * Retrieves an object from the cache using its key. + * Returns the object if it's still available (not garbage collected), + * otherwise returns null. If the object has been garbage collected, + * its entry is also removed from the cache. + * + * @param key The key of the object to retrieve. + * @return The cached object, or null if it's no longer available. + */ + fun getOrCreate( + key: K, + builder: (key: K) -> V, + ): V { + val softRef = cache.get(key) + val value = softRef?.get() + + return if (value != null) { + value + } else { + val newObject = builder(key) + cache.putIfAbsent(key, WeakReference(newObject))?.get() ?: newObject + } + } + + fun createIfAbsent( + key: K, + builder: (key: K) -> V, + ): Boolean { + val softRef = cache.get(key) + val value = softRef?.get() + return if (value != null) { + false + } else { + val newObject = builder(key) + cache.putIfAbsent(key, WeakReference(newObject)) == null + } + } + + /** + * Proactively cleans up the cache by removing entries whose weakly referenced + * objects have been garbage collected. While `get` handles cleanup on access, + * this method can be called periodically or when memory pressure is high. + */ + fun cleanUp() { + val keysToRemove = mutableListOf() + forEach { key, softRef -> + if (softRef == null) { + keysToRemove.add(key) + } + } + keysToRemove.forEach { key -> + cache.remove(key) + println("Cleaned up entry for key: $key (object was garbage collected)") + } + println("Cache cleanup completed. Remaining size: ${cache.size}") + } + + fun forEach(consumer: BiConsumer) { + innerForEach(consumer) + } + + fun filter(consumer: BiFilter): List { + val runner = BiFilterCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun filterIntoSet(consumer: BiFilter): Set { + val runner = BiFilterUniqueCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun map(consumer: BiNotNullMapper): List { + val runner = BiNotNullMapCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun mapNotNull(consumer: BiMapper): List { + val runner = BiMapCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun mapNotNullIntoSet(consumer: BiMapper): Set { + val runner = BiMapUniqueCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun mapFlatten(consumer: BiMapper?>): List { + val runner = BiMapFlattenCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun mapFlattenIntoSet(consumer: BiMapper?>): Set { + val runner = BiMapFlattenUniqueCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun maxOrNullOf( + filter: BiFilter, + comparator: Comparator, + ): V? { + val runner = BiMaxOfCollector(filter, comparator) + innerForEach(runner) + return runner.maxV + } + + fun sumOf(consumer: BiSumOf): Int { + val runner = BiSumOfCollector(consumer) + innerForEach(runner) + return runner.sum + } + + fun sumOfLong(consumer: BiSumOfLong): Long { + val runner = BiSumOfLongCollector(consumer) + innerForEach(runner) + return runner.sum + } + + fun groupBy(consumer: BiNotNullMapper): Map> { + val runner = BiGroupByCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun countByGroup(consumer: BiNotNullMapper): Map { + val runner = BiCountByGroupCollector(consumer) + innerForEach(runner) + return runner.results + } + + fun sumByGroup( + groupMap: BiNotNullMapper, + sumOf: BiNotNullMapper, + ): Map { + val runner = BiSumByGroupCollector(groupMap, sumOf) + innerForEach(runner) + return runner.results + } + + fun count(consumer: BiFilter): Int { + val runner = BiCountIfCollector(consumer) + innerForEach(runner) + return runner.count + } + + fun associate(transform: (K, V) -> Pair): Map { + val runner = BiAssociateCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + fun associateNotNull(transform: (K, V) -> Pair?): Map { + val runner = BiAssociateNotNullCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + fun associateWith(transform: (K, V) -> U?): Map { + val runner = BiAssociateWithCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + fun associateNotNullWith(transform: (K, V) -> U): Map { + val runner = BiAssociateNotNullWithCollector(size(), transform) + innerForEach(runner) + return runner.results + } + + private fun innerForEach(runner: BiConsumer) { + cache.forEach(BiConsumerWrapper(this, runner)) + } + + fun joinToString( + separator: CharSequence = ", ", + prefix: CharSequence = "", + postfix: CharSequence = "", + limit: Int = -1, + truncated: CharSequence = "...", + transform: ((K, V) -> CharSequence)? = null, + ): String { + val buffer = StringBuilder() + buffer.append(prefix) + var count = 0 + forEach { key, value -> + val str = if (transform != null) transform(key, value) else "" + if (str.isNotEmpty()) { + if (++count > 1) buffer.append(separator) + if (limit < 0 || count <= limit) { + when { + transform != null -> buffer.append(str) + else -> buffer.append("$key $value") + } + } else { + return@forEach + } + } + } + if (limit >= 0 && count > limit) buffer.append(truncated) + buffer.append(postfix) + return buffer.toString() + } + + class BiConsumerWrapper( + val cache: LargeSoftCache, + val inner: BiConsumer, + ) : BiConsumer> { + override fun accept( + k: K, + ref: WeakReference, + ) { + val value = ref.get() + if (value == null) { + cache.remove(k) + } else { + inner.accept(k, value) + } + } + } + + fun interface BiFilter { + fun filter( + k: K, + v: V, + ): Boolean + } + + class BiFilterCollector( + val filter: BiFilter, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + results.add(v) + } + } + } + + class BiFilterUniqueCollector( + val filter: BiFilter, + ) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + results.add(v) + } + } + } + + fun interface BiMapper { + fun map( + k: K, + v: V, + ): R? + } + + fun interface BiMapperNotNull { + fun map( + k: K, + v: V, + ): R + } + + class BiMapCollector( + val mapper: BiMapper, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.add(result) + } + } + } + + class BiAssociateCollector( + val size: Int, + val mapper: BiMapperNotNull>, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val pair = mapper.map(k, v) + results.put(pair.first, pair.second) + } + } + + class BiAssociateNotNullCollector( + val size: Int, + val mapper: BiMapper?>, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val pair = mapper.map(k, v) + if (pair != null) { + results.put(pair.first, pair.second) + } + } + } + + class BiAssociateWithCollector( + val size: Int, + val mapper: BiMapper, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + results.put(k, mapper.map(k, v)) + } + } + + class BiAssociateNotNullWithCollector( + val size: Int, + val mapper: BiMapper, + ) : BiConsumer { + var results: LinkedHashMap = LinkedHashMap(size) + + override fun accept( + k: K, + v: V, + ) { + val newValue = mapper.map(k, v) + if (newValue != null) { + results.put(k, newValue) + } + } + } + + class BiMapUniqueCollector( + val mapper: BiMapper, + ) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.add(result) + } + } + } + + class BiMapFlattenCollector( + val mapper: BiMapper?>, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.addAll(result) + } + } + } + + class BiMapFlattenUniqueCollector( + val mapper: BiMapper?>, + ) : BiConsumer { + var results: HashSet = HashSet() + + override fun accept( + k: K, + v: V, + ) { + val result = mapper.map(k, v) + if (result != null) { + results.addAll(result) + } + } + } + + fun interface BiNotNullMapper { + fun map( + k: K, + v: V, + ): R + } + + class BiNotNullMapCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results: ArrayList = ArrayList() + + override fun accept( + k: K, + v: V, + ) { + results.add(mapper.map(k, v)) + } + } + + fun interface BiSumOf { + fun map( + k: K, + v: V, + ): Int + } + + class BiMaxOfCollector( + val filter: BiFilter, + val comparator: Comparator, + ) : BiConsumer { + var maxK: K? = null + var maxV: V? = null + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) { + if (maxK == null || comparator.compare(v, maxV) > 0) { + maxK = k + maxV = v + } + } + } + } + + class BiSumOfCollector( + val mapper: BiSumOf, + ) : BiConsumer { + var sum = 0 + + override fun accept( + k: K, + v: V, + ) { + sum += mapper.map(k, v) + } + } + + fun interface BiSumOfLong { + fun map( + k: K, + v: V, + ): Long + } + + class BiSumOfLongCollector( + val mapper: BiSumOfLong, + ) : BiConsumer { + var sum = 0L + + override fun accept( + k: K, + v: V, + ) { + sum += mapper.map(k, v) + } + } + + class BiGroupByCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap>() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val list = results[group] + if (list == null) { + val answer = ArrayList() + answer.add(v) + results[group] = answer + } else { + list.add(v) + } + } + } + + class BiCountByGroupCollector( + val mapper: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val count = results[group] + if (count == null) { + results[group] = 1 + } else { + results[group] = count + 1 + } + } + } + + class BiSumByGroupCollector( + val mapper: BiNotNullMapper, + val sumOf: BiNotNullMapper, + ) : BiConsumer { + var results = HashMap() + + override fun accept( + k: K, + v: V, + ) { + val group = mapper.map(k, v) + + val sum = results[group] + if (sum == null) { + results[group] = sumOf.map(k, v) + } else { + results[group] = sum + sumOf.map(k, v) + } + } + } + + class BiCountIfCollector( + val filter: BiFilter, + ) : BiConsumer { + var count = 0 + + override fun accept( + k: K, + v: V, + ) { + if (filter.filter(k, v)) count++ + } + } +} From 5ac4314e2a0ddc682dc4e872b43c1ef239bca3e0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 23 Jul 2025 11:22:50 -0400 Subject: [PATCH 122/185] Improvements to the online check --- .../amethyst/ui/actions/MediaSaverToDisk.kt | 4 +- .../ui/screen/loggedIn/AccountViewModel.kt | 12 ++---- .../ui/screen/loggedIn/home/HomeScreen.kt | 39 ++++++++----------- 3 files changed, 22 insertions(+), 33 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt index 544cab2176..8a02c884f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/MediaSaverToDisk.kt @@ -54,9 +54,9 @@ object MediaSaverToDisk { localContext: Context, onSuccess: () -> Any?, onError: (Throwable) -> Any?, - ) { + ) = withContext(Dispatchers.IO) { when { - videoUri.isNullOrBlank() -> return + videoUri.isNullOrBlank() -> return@withContext videoUri.startsWith("file") -> save( localFile = videoUri.toUri().toFile(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 664fd88150..9b72ca41be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1158,16 +1158,10 @@ class AccountViewModel( .sortedBy { account.isFollowing(it) } .reversed() - fun checkVideoIsOnline( - videoUrl: String, - onDone: (Boolean) -> Unit, - ) { - viewModelScope.launch(Dispatchers.IO) { - onDone( - OnlineChecker.isOnline(videoUrl, ::okHttpClientForVideo), - ) + suspend fun checkVideoIsOnline(videoUrl: String): Boolean = + withContext(Dispatchers.IO) { + OnlineChecker.isOnline(videoUrl, ::okHttpClientForVideo) } - } fun loadAndMarkAsRead( routeForLastRead: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index c3cd55dedf..dab1e76605 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -57,6 +58,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AROUND_ME import com.vitorpamplona.amethyst.service.OnlineChecker +import com.vitorpamplona.amethyst.service.OnlineChecker.isOnline import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState @@ -90,6 +92,7 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch +import kotlin.collections.forEachIndexed @Composable fun HomeScreen( @@ -375,17 +378,13 @@ fun CheckIfVideoIsOnline( accountViewModel: AccountViewModel, whenOnline: @Composable (Boolean) -> Unit, ) { - var online by remember { - mutableStateOf( - OnlineChecker.isOnlineCached(url), - ) - } - - LaunchedEffect(key1 = url) { - accountViewModel.checkVideoIsOnline(url) { isOnline -> - if (online != isOnline) { - online = isOnline - } + val online by produceState( + initialValue = OnlineChecker.isOnlineCached(url), + key1 = url, + ) { + val isOnline = accountViewModel.checkVideoIsOnline(url) + if (value != isOnline) { + value = isOnline } } @@ -398,17 +397,13 @@ fun CrossfadeCheckIfVideoIsOnline( accountViewModel: AccountViewModel, whenOnline: @Composable () -> Unit, ) { - var online by remember { - mutableStateOf( - OnlineChecker.isOnlineCached(url), - ) - } - - LaunchedEffect(key1 = url) { - accountViewModel.checkVideoIsOnline(url) { isOnline -> - if (online != isOnline) { - online = isOnline - } + val online by produceState( + initialValue = OnlineChecker.isOnlineCached(url), + key1 = url, + ) { + val isOnline = accountViewModel.checkVideoIsOnline(url) + if (value != isOnline) { + value = isOnline } } From 57e4d76da05524fda7319e044ba8c0be78f4c588 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 23 Jul 2025 11:35:25 -0400 Subject: [PATCH 123/185] Adds broadcasting relay section --- .../vitorpamplona/amethyst/model/Account.kt | 13 +- .../amethyst/model/LocalCache.kt | 8 ++ .../AccountOutboxRelayState.kt | 10 +- .../BroadcastRelayListDecryptionCache.kt | 29 +++++ .../BroadcastRelayListState.kt | 87 +++++++++++++ .../MergedFollowPlusMineRelayListsState.kt | 5 + .../serverList/TrustedRelayListsState.kt | 5 + .../FilterAccountInfoAndListsFromKey.kt | 2 + .../FilterBasicAccountInfoFromKeys.kt | 18 ++- .../amethyst/ui/note/NoteCompose.kt | 3 + .../amethyst/ui/note/types/RelayList.kt | 21 +++ .../loggedIn/relays/AllRelayListScreen.kt | 19 +++ .../broadcast/BroadcastRelayListViewModel.kt | 35 +++++ .../relays/broadcast/TrustedRelayListView.kt | 81 ++++++++++++ .../loggedIn/threadview/ThreadFeedView.kt | 4 + amethyst/src/main/res/values/strings.xml | 9 +- .../com/vitorpamplona/quartz/EventFactory.kt | 2 + .../relayLists/BroadcastRelayListEvent.kt | 121 ++++++++++++++++++ .../relayLists/tags/TagArrayBuilderExt.kt | 3 + 19 files changed, 465 insertions(+), 10 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 46367f35ed..ad3c409d2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -54,6 +54,8 @@ import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.BlockPeopleLi import com.vitorpamplona.amethyst.model.nip51Lists.blockPeopleList.PeopleListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache @@ -242,6 +244,9 @@ class Account( val trustedRelayListDecryptionCache = TrustedRelayListDecryptionCache(signer) val trustedRelayList = TrustedRelayListState(signer, cache, trustedRelayListDecryptionCache, scope, settings) + val broadcastRelayListDecryptionCache = BroadcastRelayListDecryptionCache(signer) + val broadcastRelayList = BroadcastRelayListState(signer, cache, broadcastRelayListDecryptionCache, scope, settings) + val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer) val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings) @@ -280,15 +285,15 @@ class Account( val serverLists = MergedServerListState(fileStorageServers, blossomServers, scope) // Relay settings - val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, scope) + val outboxRelays = AccountOutboxRelayState(nip65RelayList, privateStorageRelayList, localRelayList, broadcastRelayList, scope) val dmRelays = DmInboxRelayState(dmRelayList, nip65RelayList, privateStorageRelayList, localRelayList, scope) val notificationRelays = NotificationInboxRelayState(nip65RelayList, localRelayList, scope) - val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, trustedRelayList, scope) + val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, trustedRelayList, broadcastRelayList, scope) // Follows Relays val followOutboxes = FollowListOutboxRelays(kind3FollowList, blockedRelayList, cache, scope) - val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, trustedRelayList, scope) + val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, trustedRelayList, broadcastRelayList, scope) // keeps a cache of the outbox relays for each author val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow @@ -1714,6 +1719,8 @@ class Account( suspend fun saveSearchRelayList(searchRelays: List) = sendMyPublicAndPrivateOutbox(searchRelayList.saveRelayList(searchRelays)) + suspend fun saveBroadcastRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(broadcastRelayList.saveRelayList(trustedRelays)) + suspend fun saveTrustedRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(trustedRelayList.saveRelayList(trustedRelays)) suspend fun saveBlockedRelayList(blockedRelays: List) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index a5504a4a3b..5ce63e6584 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -138,6 +138,7 @@ import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -957,6 +958,12 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( + event: BroadcastRelayListEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( event: CommunityDefinitionEvent, relay: NormalizedRelayUrl?, @@ -2709,6 +2716,7 @@ object LocalCache : ILocalCache { is BadgeProfilesEvent -> consume(event, relay, wasVerified) is BlockedRelayListEvent -> consume(event, relay, wasVerified) is BlossomServersEvent -> consume(event, relay, wasVerified) + is BroadcastRelayListEvent -> consume(event, relay, wasVerified) is BookmarkListEvent -> consume(event, relay, wasVerified) is CalendarEvent -> consume(event, relay, wasVerified) is CalendarDateSlotEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt index 001e086ac2..58de33ab47 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/AccountOutboxRelayState.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model.nip01UserMetadata import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -34,6 +35,7 @@ class AccountOutboxRelayState( nip65: Nip65RelayListState, privateStorage: PrivateStorageRelayListState, local: LocalRelayListState, + broadcast: BroadcastRelayListState, scope: CoroutineScope, ) { val flow = @@ -41,14 +43,16 @@ class AccountOutboxRelayState( nip65.outboxFlow, privateStorage.flow, local.flow, - ) { nip65Inbox, privateOutBox, localRelays -> - nip65Inbox + privateOutBox + localRelays + broadcast.flow, + ) { nip65Inbox, privateOutBox, localRelays, broadcastRelays -> + nip65Inbox + privateOutBox + localRelays + broadcastRelays }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, nip65.outboxFlow.value + privateStorage.flow.value + - local.flow.value, + local.flow.value + + broadcast.flow.value, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt new file mode 100644 index 0000000000..73ba129060 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListDecryptionCache.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays + +import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent + +class BroadcastRelayListDecryptionCache( + signer: NostrSigner, +) : GenericRelayListCache(signer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt new file mode 100644 index 0000000000..8cada24fe4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/broadcastRelays/BroadcastRelayListState.kt @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class BroadcastRelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val decryptionCache: BroadcastRelayListDecryptionCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getBroadcastRelayListAddress() = BroadcastRelayListEvent.createAddress(signer.pubKey) + + fun getBroadcastRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getBroadcastRelayListAddress()) + + fun getBroadcastRelayListFlow(): StateFlow = getBroadcastRelayListNote().flow().metadata.stateFlow + + fun getBroadcastRelayList(): BroadcastRelayListEvent? = getBroadcastRelayListNote().event as? BroadcastRelayListEvent + + suspend fun normalizeBroadcastRelayListWithBackup(note: Note): Set { + val event = note.event as? BroadcastRelayListEvent + return event?.let { decryptionCache.relays(it) } ?: emptySet() + } + + val flow = + getBroadcastRelayListFlow() + .map { normalizeBroadcastRelayListWithBackup(it.note) } + .onStart { emit(normalizeBroadcastRelayListWithBackup(getBroadcastRelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + suspend fun saveRelayList(broadcastRelays: List): BroadcastRelayListEvent { + val relayListForBroadcast = getBroadcastRelayList() + + return if (relayListForBroadcast != null && relayListForBroadcast.tags.isNotEmpty()) { + BroadcastRelayListEvent.updateRelayList( + earlierVersion = relayListForBroadcast, + relays = broadcastRelays, + signer = signer, + ) + } else { + BroadcastRelayListEvent.create( + relays = broadcastRelays, + signer = signer, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index b016b1eaec..1f2a6fa7c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays +import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -41,6 +42,7 @@ class MergedFollowPlusMineRelayListsState( val privateOutboxRelayList: PrivateStorageRelayListState, val localRelayList: LocalRelayListState, val trustedRelayList: TrustedRelayListState, + val broadcastRelayList: BroadcastRelayListState, val scope: CoroutineScope, ) { fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } @@ -54,6 +56,7 @@ class MergedFollowPlusMineRelayListsState( privateOutboxRelayList.flow, localRelayList.flow, trustedRelayList.flow, + broadcastRelayList.flow, ), ::mergeLists, ).onStart { @@ -66,6 +69,7 @@ class MergedFollowPlusMineRelayListsState( privateOutboxRelayList.flow.value, localRelayList.flow.value, trustedRelayList.flow.value, + broadcastRelayList.flow.value, ), ), ) @@ -81,6 +85,7 @@ class MergedFollowPlusMineRelayListsState( privateOutboxRelayList.flow.value, localRelayList.flow.value, trustedRelayList.flow.value, + broadcastRelayList.flow.value, ), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt index 77e2797426..743481c340 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/TrustedRelayListsState.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState @@ -43,6 +44,7 @@ class TrustedRelayListsState( val dmRelayList: DmRelayListState, val searchRelayListState: SearchRelayListState, val trustedRelayList: TrustedRelayListState, + val broadcastRelayList: BroadcastRelayListState, val scope: CoroutineScope, ) { fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } @@ -56,6 +58,7 @@ class TrustedRelayListsState( dmRelayList.flow, searchRelayListState.flow, trustedRelayList.flow, + broadcastRelayList.flow, ), ::mergeLists, ).onStart { @@ -68,6 +71,7 @@ class TrustedRelayListsState( dmRelayList.flow.value, searchRelayListState.flow.value, trustedRelayList.flow.value, + broadcastRelayList.flow.value, ), ), ) @@ -83,6 +87,7 @@ class TrustedRelayListsState( dmRelayList.flow.value, searchRelayListState.flow.value, trustedRelayList.flow.value, + broadcastRelayList.flow.value, ), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 774bb3bded..51a97c89cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayList import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent @@ -55,6 +56,7 @@ val AccountInfoAndListsFromKeyKinds2 = listOf( BlockedRelayListEvent.KIND, TrustedRelayListEvent.KIND, + BroadcastRelayListEvent.KIND, ) val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt index 34dff46d7d..d988c7b124 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent @@ -43,8 +44,13 @@ val BasicAccountInfoKinds = SearchRelayListEvent.KIND, FileServersEvent.KIND, BlossomServersEvent.KIND, + ) + +val BasicAccountInfoKinds2 = + listOf( BlockedRelayListEvent.KIND, TrustedRelayListEvent.KIND, + BroadcastRelayListEvent.KIND, ) fun filterBasicAccountInfoFromKeys( @@ -61,7 +67,17 @@ fun filterBasicAccountInfoFromKeys( Filter( kinds = BasicAccountInfoKinds, authors = otherAccounts.toList(), - limit = otherAccounts.size * 8, + limit = otherAccounts.size * BasicAccountInfoKinds.size, + since = since, + ), + ), + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = BasicAccountInfoKinds2, + authors = otherAccounts.toList(), + limit = otherAccounts.size * BasicAccountInfoKinds2.size, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 42fcd2704c..675860072e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -79,6 +79,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList +import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList @@ -187,6 +188,7 @@ import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -654,6 +656,7 @@ private fun RenderNoteRow( is SearchRelayListEvent -> DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav) is BlockedRelayListEvent -> DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) is TrustedRelayListEvent -> DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) + is BroadcastRelayListEvent -> DisplayBroadcastRelayList(baseNote, backgroundColor, accountViewModel, nav) is PinListEvent -> RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav) is EmojiPackEvent -> RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel) is LiveActivitiesEvent -> RenderLiveActivityEvent(baseNote, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index d8382e3055..2cb155e280 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -234,6 +234,27 @@ fun DisplayTrustedRelayList( ) } +@Composable +fun DisplayBroadcastRelayList( + baseNote: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val relays by accountViewModel.account.broadcastRelayListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle( + accountViewModel.account.broadcastRelayListDecryptionCache.fastStartValueForRelayList(baseNote), + ) + + DisplayRelaySet( + relays, + stringRes(id = R.string.broadcast_relays_title), + null, + backgroundColor, + accountViewModel, + nav, + ) +} + @Composable fun DisplayRelaySet( relay: RelayListCard, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index ab81f80bc6..35a1897016 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -47,6 +47,8 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.BroadcastRelayListViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.renderBroadcastItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems @@ -83,6 +85,7 @@ fun AllRelayListScreen( val trustedViewModel: TrustedRelayListViewModel = viewModel() val localViewModel: LocalRelayListViewModel = viewModel() val connectedViewModel: ConnectedRelayListViewModel = viewModel() + val broadcastViewModel: BroadcastRelayListViewModel = viewModel() dmViewModel.init(accountViewModel) nip65ViewModel.init(accountViewModel) @@ -92,6 +95,7 @@ fun AllRelayListScreen( connectedViewModel.init(accountViewModel) blockedViewModel.init(accountViewModel) trustedViewModel.init(accountViewModel) + broadcastViewModel.init(accountViewModel) LaunchedEffect(accountViewModel) { dmViewModel.load() @@ -102,6 +106,7 @@ fun AllRelayListScreen( connectedViewModel.load() blockedViewModel.load() trustedViewModel.load() + broadcastViewModel.load() } MappedAllRelayListView( @@ -113,6 +118,7 @@ fun AllRelayListScreen( connectedViewModel, blockedViewModel, trustedViewModel, + broadcastViewModel, accountViewModel, nav, ) @@ -129,6 +135,7 @@ fun MappedAllRelayListView( connectedViewModel: ConnectedRelayListViewModel, blockedViewModel: BlockedRelayListViewModel, trustedViewModel: TrustedRelayListViewModel, + broadcastViewModel: BroadcastRelayListViewModel, accountViewModel: AccountViewModel, newNav: INav, ) { @@ -141,6 +148,7 @@ fun MappedAllRelayListView( val trustedFeedState by trustedViewModel.relays.collectAsStateWithLifecycle() val localFeedState by localViewModel.relays.collectAsStateWithLifecycle() val connectedRelays by connectedViewModel.relays.collectAsStateWithLifecycle() + val broadcastRelays by broadcastViewModel.relays.collectAsStateWithLifecycle() Scaffold( topBar = { @@ -154,6 +162,7 @@ fun MappedAllRelayListView( privateOutboxViewModel.clear() trustedViewModel.clear() blockedViewModel.clear() + broadcastViewModel.clear() newNav.popBack() }, onPost = { @@ -164,6 +173,7 @@ fun MappedAllRelayListView( privateOutboxViewModel.create() trustedViewModel.create() blockedViewModel.create() + broadcastViewModel.create() newNav.popBack() }, ) @@ -221,6 +231,15 @@ fun MappedAllRelayListView( } renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, newNav) + item { + SettingsCategory( + stringRes(R.string.broadcast_section), + stringRes(R.string.broadcast_section_explainer), + SettingsCategorySpacingModifier, + ) + } + renderBroadcastItems(broadcastRelays, broadcastViewModel, accountViewModel, newNav) + item { SettingsCategoryWithButton( stringRes(R.string.search_section), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt new file mode 100644 index 0000000000..fb36e2080e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListViewModel.kt @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import java.util.stream.Collectors.toList + +class BroadcastRelayListViewModel : BasicRelaySetupInfoModel() { + override fun getRelayList(): List? = + account.broadcastRelayList.flow.value + .toList() + + override suspend fun saveRelayList(urlList: List) { + account.saveBroadcastRelayList(urlList) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt new file mode 100644 index 0000000000..5fd691e581 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer + +@Composable +fun BroadcastRelayList( + postViewModel: BroadcastRelayListViewModel, + accountViewModel: AccountViewModel, + onClose: () -> Unit, + nav: INav, +) { + val feedState by postViewModel.relays.collectAsStateWithLifecycle() + + val newNav = rememberExtendedNav(nav, onClose) + + Row(verticalAlignment = Alignment.CenterVertically) { + LazyColumn( + contentPadding = FeedPadding, + ) { + renderBroadcastItems(feedState, postViewModel, accountViewModel, newNav) + } + } +} + +fun LazyListScope.renderBroadcastItems( + feedState: List, + postViewModel: BroadcastRelayListViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + itemsIndexed(feedState, key = { _, item -> "Broadcast" + item.relay }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + accountViewModel = accountViewModel, + nav, + ) + } + + item { + Spacer(modifier = StdVertSpacer) + RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index c04fd1310c..e22eb57ee6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -119,6 +119,7 @@ import com.vitorpamplona.amethyst.ui.note.types.AudioHeader import com.vitorpamplona.amethyst.ui.note.types.AudioTrackHeader import com.vitorpamplona.amethyst.ui.note.types.BadgeDisplay import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList +import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList @@ -208,6 +209,7 @@ import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -603,6 +605,8 @@ private fun FullBleedNoteCompose( DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is TrustedRelayListEvent) { DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) + } else if (noteEvent is BroadcastRelayListEvent) { + DisplayBroadcastRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is FhirResourceEvent) { RenderFhirResource(baseNote, accountViewModel, nav) } else if (noteEvent is GitRepositoryEvent) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index a9cdda2ec5..89d142a5f0 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1064,9 +1064,6 @@ Insert between 1–3 relays to use when searching for content or tagging users. Make sure your chosen relays implement NIP-50 Good options are:\n - nostr.wine\n - relay.nostr.band\n - relay.noswhere.com - Blocked Relays - Trusted Relays - DM Upload Relay Settings Public Outbox/Home Relays @@ -1091,9 +1088,15 @@ Local Relays List of relays that are running in this device. + Trusted Relays Trusted Relays Relays you trust to not need a Tor connection for + Broadcast Relays + Broadcast Relays + Relays that specialize in pushing your notes to all of the other relays. Amethyst will add this relay to all new events made by you + + Blocked Relays Blocked Relays Amethyst will never connect to these relays diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 0f14d4abcd..6715c03687 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -82,6 +82,7 @@ import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -161,6 +162,7 @@ class EventFactory { BlockedRelayListEvent.KIND -> BlockedRelayListEvent(id, pubKey, createdAt, tags, content, sig) BlossomServersEvent.KIND -> BlossomServersEvent(id, pubKey, createdAt, tags, content, sig) BlossomAuthorizationEvent.KIND -> BlossomAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) + BroadcastRelayListEvent.KIND -> BroadcastRelayListEvent(id, pubKey, createdAt, tags, content, sig) BookmarkListEvent.KIND -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig) CalendarDateSlotEvent.KIND -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig) CalendarEvent.KIND -> CalendarEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt new file mode 100644 index 0000000000..f09930df0b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.relayLists + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.broadcastRelays +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relays +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.plus + +@Immutable +class BroadcastRelayListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicRelays() = tags.relays() + + suspend fun decryptPrivateRelays(signer: NostrSigner) = privateTags(signer)?.relays() + + suspend fun decryptRelays(signer: NostrSigner): List = publicRelays() + (decryptPrivateRelays(signer) ?: emptyList()) + + companion object { + const val KIND = 10088 + const val ALT = "Broadcasting relays from this author" + val TAGS = arrayOf(AltTag.assemble(ALT)) + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "") + + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null) + + fun createAddressTag(pubKey: HexKey): String = Address.Companion.assemble(KIND, pubKey, "") + + suspend fun updateRelayList( + earlierVersion: BroadcastRelayListEvent, + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): BroadcastRelayListEvent { + val newRelayList = relays.map { RelayTag.assemble(it) } + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + + val publicTags = earlierVersion.tags.remove(RelayTag::match) + val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) + + return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags) + } + + suspend fun create( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): BroadcastRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun create( + relays: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): BroadcastRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun build( + publicRelays: List = emptyList(), + privateRelays: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + broadcastRelays(publicRelays) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt index b8626bf5fe..c6cb3e2e47 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayList import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.tags.NameTag @@ -39,6 +40,8 @@ fun TagArrayBuilder.blockedRelays(relays: List.trustedRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) +fun TagArrayBuilder.broadcastRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) + fun TagArrayBuilder.privateRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) fun TagArrayBuilder.relaySet(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) From 1084d2a063d59d68e5acc09b199553e65e56f905 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 23 Jul 2025 13:19:46 -0400 Subject: [PATCH 124/185] Adds an indexer relay list --- .../vitorpamplona/amethyst/model/Account.kt | 11 +- .../amethyst/model/AccountSettings.kt | 2 + .../vitorpamplona/amethyst/model/Constants.kt | 4 + .../amethyst/model/LocalCache.kt | 8 ++ .../IndexerRelayListDecryptionCache.kt | 29 +++++ .../indexerRelays/IndexerRelayListState.kt | 88 +++++++++++++ .../MergedFollowPlusMineRelayListsState.kt | 5 + .../FilterAccountInfoAndListsFromKey.kt | 2 + .../FilterBasicAccountInfoFromKeys.kt | 2 + .../amethyst/ui/note/NoteCompose.kt | 3 + .../amethyst/ui/note/types/RelayList.kt | 21 +++ .../loggedIn/relays/AllRelayListScreen.kt | 39 ++++++ ...yListView.kt => BroadcastRelayListView.kt} | 0 .../relays/indexer/IndexerRelayListView.kt | 81 ++++++++++++ .../indexer/IndexerRelayListViewModel.kt | 34 +++++ .../loggedIn/threadview/ThreadFeedView.kt | 4 + amethyst/src/main/res/values/strings.xml | 6 +- .../com/vitorpamplona/quartz/EventFactory.kt | 2 + .../nip01Core/signers/NostrSignerSync.kt | 2 + .../relayLists/BroadcastRelayListEvent.kt | 5 +- .../relayLists/IndexerRelayListEvent.kt | 121 ++++++++++++++++++ .../relayLists/tags/TagArrayBuilderExt.kt | 3 + .../client/NostrSignerExternal.kt | 4 + 23 files changed, 471 insertions(+), 5 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/{TrustedRelayListView.kt => BroadcastRelayListView.kt} (100%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ad3c409d2b..bbe9163473 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -60,6 +60,8 @@ import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecry import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState +import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache @@ -247,6 +249,9 @@ class Account( val broadcastRelayListDecryptionCache = BroadcastRelayListDecryptionCache(signer) val broadcastRelayList = BroadcastRelayListState(signer, cache, broadcastRelayListDecryptionCache, scope, settings) + val indexerRelayListDecryptionCache = IndexerRelayListDecryptionCache(signer) + val indexerRelayList = IndexerRelayListState(signer, cache, indexerRelayListDecryptionCache, scope, settings) + val blockedRelayListDecryptionCache = BlockedRelayListDecryptionCache(signer) val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings) @@ -293,7 +298,7 @@ class Account( // Follows Relays val followOutboxes = FollowListOutboxRelays(kind3FollowList, blockedRelayList, cache, scope) - val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, trustedRelayList, broadcastRelayList, scope) + val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, trustedRelayList, broadcastRelayList, indexerRelayList, scope) // keeps a cache of the outbox relays for each author val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow @@ -871,7 +876,7 @@ class Account( } fun sendLiterallyEverywhere(event: Event) { - client.send(event, outboxRelays.flow.value + client.relayStatusFlow().value.available) + client.send(event, outboxRelays.flow.value + indexerRelayList.flow.value + client.relayStatusFlow().value.available) cache.justConsumeMyOwnEvent(event) } @@ -1719,6 +1724,8 @@ class Account( suspend fun saveSearchRelayList(searchRelays: List) = sendMyPublicAndPrivateOutbox(searchRelayList.saveRelayList(searchRelays)) + suspend fun saveIndexerRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(indexerRelayList.saveRelayList(trustedRelays)) + suspend fun saveBroadcastRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(broadcastRelayList.saveRelayList(trustedRelays)) suspend fun saveTrustedRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(trustedRelayList.saveRelayList(trustedRelays)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 0c0b467fab..120281127a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -84,6 +84,8 @@ val DefaultDMRelayList = listOf(Constants.auth, Constants.oxchat, Constants.nos) val DefaultSearchRelayList = setOf(Constants.band, Constants.wine, Constants.where, Constants.nostoday) +val DefaultIndexerRelayList = setOf(Constants.purplepages, Constants.coracle, Constants.userkinds) + val DefaultSignerPermissions = listOf( Permission(CommandType.SIGN_EVENT, RelayAuthEvent.KIND), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt index ec6a49a481..d34c7d42c6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Constants.kt @@ -43,6 +43,10 @@ object Constants { val auth = RelayUrlNormalizer.normalize("wss://auth.nostr1.com") val oxchat = RelayUrlNormalizer.normalize("wss://relay.0xchat.com") + val purplepages = RelayUrlNormalizer.normalize("wss://purplepag.es") + val coracle = RelayUrlNormalizer.normalize("wss://indexer.coracle.social") + val userkinds = RelayUrlNormalizer.normalize("wss://user.kindpag.es") + val eventFinderRelays = setOf(band, wine, damus, primal, mom, nos, bitcoiner, oxtr, fmtwiz, bg) val defaultSearchRelaySet = setOf(band, wine, where) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 5ce63e6584..3c68f850ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -139,6 +139,7 @@ import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -958,6 +959,12 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( + event: IndexerRelayListEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( event: BroadcastRelayListEvent, relay: NormalizedRelayUrl?, @@ -2760,6 +2767,7 @@ object LocalCache : ILocalCache { is GitRepositoryEvent -> consume(event, relay, wasVerified) is HashtagListEvent -> consume(event, relay, wasVerified) is HighlightEvent -> consume(event, relay, wasVerified) + is IndexerRelayListEvent -> consume(event, relay, wasVerified) is InteractiveStoryPrologueEvent -> consume(event, relay, wasVerified) is InteractiveStorySceneEvent -> consume(event, relay, wasVerified) is InteractiveStoryReadingStateEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt new file mode 100644 index 0000000000..13d1ce9d4a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListDecryptionCache.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays + +import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent + +class IndexerRelayListDecryptionCache( + signer: NostrSigner, +) : GenericRelayListCache(signer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt new file mode 100644 index 0000000000..5fa08599a1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListDecryptionCache +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class IndexerRelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val decryptionCache: IndexerRelayListDecryptionCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getIndexerRelayListAddress() = IndexerRelayListEvent.createAddress(signer.pubKey) + + fun getIndexerRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getIndexerRelayListAddress()) + + fun getIndexerRelayListFlow(): StateFlow = getIndexerRelayListNote().flow().metadata.stateFlow + + fun getIndexerRelayList(): IndexerRelayListEvent? = getIndexerRelayListNote().event as? IndexerRelayListEvent + + suspend fun normalizeIndexerRelayListWithBackup(note: Note): Set { + val event = note.event as? IndexerRelayListEvent + return event?.let { decryptionCache.relays(it) } ?: emptySet() + } + + val flow = + getIndexerRelayListFlow() + .map { normalizeIndexerRelayListWithBackup(it.note) } + .onStart { emit(normalizeIndexerRelayListWithBackup(getIndexerRelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + suspend fun saveRelayList(indexerRelays: List): IndexerRelayListEvent { + val relayListForIndexer = getIndexerRelayList() + + return if (relayListForIndexer != null && relayListForIndexer.tags.isNotEmpty()) { + IndexerRelayListEvent.updateRelayList( + earlierVersion = relayListForIndexer, + relays = indexerRelays, + signer = signer, + ) + } else { + IndexerRelayListEvent.create( + relays = indexerRelays, + signer = signer, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index 1f2a6fa7c7..da18f2185a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -43,6 +44,7 @@ class MergedFollowPlusMineRelayListsState( val localRelayList: LocalRelayListState, val trustedRelayList: TrustedRelayListState, val broadcastRelayList: BroadcastRelayListState, + val indexerRelayList: IndexerRelayListState, val scope: CoroutineScope, ) { fun mergeLists(lists: Array>): Set = lists.reduce { acc, set -> acc + set } @@ -57,6 +59,7 @@ class MergedFollowPlusMineRelayListsState( localRelayList.flow, trustedRelayList.flow, broadcastRelayList.flow, + indexerRelayList.flow, ), ::mergeLists, ).onStart { @@ -70,6 +73,7 @@ class MergedFollowPlusMineRelayListsState( localRelayList.flow.value, trustedRelayList.flow.value, broadcastRelayList.flow.value, + indexerRelayList.flow.value, ), ), ) @@ -86,6 +90,7 @@ class MergedFollowPlusMineRelayListsState( localRelayList.flow.value, trustedRelayList.flow.value, broadcastRelayList.flow.value, + indexerRelayList.flow.value, ), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 51a97c89cc..099e3d677d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent @@ -57,6 +58,7 @@ val AccountInfoAndListsFromKeyKinds2 = BlockedRelayListEvent.KIND, TrustedRelayListEvent.KIND, BroadcastRelayListEvent.KIND, + IndexerRelayListEvent.KIND, ) val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt index d988c7b124..2861de76fd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent @@ -51,6 +52,7 @@ val BasicAccountInfoKinds2 = BlockedRelayListEvent.KIND, TrustedRelayListEvent.KIND, BroadcastRelayListEvent.KIND, + IndexerRelayListEvent.KIND, ) fun filterBasicAccountInfoFromKeys( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 675860072e..0f9de6c694 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList +import com.vitorpamplona.amethyst.ui.note.types.DisplayIndexerRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet @@ -189,6 +190,7 @@ import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -656,6 +658,7 @@ private fun RenderNoteRow( is SearchRelayListEvent -> DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav) is BlockedRelayListEvent -> DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) is TrustedRelayListEvent -> DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) + is IndexerRelayListEvent -> DisplayIndexerRelayList(baseNote, backgroundColor, accountViewModel, nav) is BroadcastRelayListEvent -> DisplayBroadcastRelayList(baseNote, backgroundColor, accountViewModel, nav) is PinListEvent -> RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav) is EmojiPackEvent -> RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index 2cb155e280..afe8555f12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -234,6 +234,27 @@ fun DisplayTrustedRelayList( ) } +@Composable +fun DisplayIndexerRelayList( + baseNote: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val relays by accountViewModel.account.indexerRelayListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle( + accountViewModel.account.indexerRelayListDecryptionCache.fastStartValueForRelayList(baseNote), + ) + + DisplayRelaySet( + relays, + stringRes(id = R.string.indexer_relays_title), + null, + backgroundColor, + accountViewModel, + nav, + ) +} + @Composable fun DisplayBroadcastRelayList( baseNote: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 35a1897016..f668c96e9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -41,6 +41,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.DefaultDMRelayList +import com.vitorpamplona.amethyst.model.DefaultIndexerRelayList import com.vitorpamplona.amethyst.model.DefaultSearchRelayList import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar @@ -54,6 +55,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.ConnectedR import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.connected.renderConnectedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.renderDMItems +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer.IndexerRelayListViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer.renderIndexerItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.LocalRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.renderLocalItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37.PrivateOutboxRelayListViewModel @@ -86,6 +89,7 @@ fun AllRelayListScreen( val localViewModel: LocalRelayListViewModel = viewModel() val connectedViewModel: ConnectedRelayListViewModel = viewModel() val broadcastViewModel: BroadcastRelayListViewModel = viewModel() + val indexerViewModel: IndexerRelayListViewModel = viewModel() dmViewModel.init(accountViewModel) nip65ViewModel.init(accountViewModel) @@ -96,6 +100,7 @@ fun AllRelayListScreen( blockedViewModel.init(accountViewModel) trustedViewModel.init(accountViewModel) broadcastViewModel.init(accountViewModel) + indexerViewModel.init(accountViewModel) LaunchedEffect(accountViewModel) { dmViewModel.load() @@ -107,6 +112,7 @@ fun AllRelayListScreen( blockedViewModel.load() trustedViewModel.load() broadcastViewModel.load() + indexerViewModel.load() } MappedAllRelayListView( @@ -119,6 +125,7 @@ fun AllRelayListScreen( blockedViewModel, trustedViewModel, broadcastViewModel, + indexerViewModel, accountViewModel, nav, ) @@ -136,6 +143,7 @@ fun MappedAllRelayListView( blockedViewModel: BlockedRelayListViewModel, trustedViewModel: TrustedRelayListViewModel, broadcastViewModel: BroadcastRelayListViewModel, + indexerViewModel: IndexerRelayListViewModel, accountViewModel: AccountViewModel, newNav: INav, ) { @@ -149,6 +157,7 @@ fun MappedAllRelayListView( val localFeedState by localViewModel.relays.collectAsStateWithLifecycle() val connectedRelays by connectedViewModel.relays.collectAsStateWithLifecycle() val broadcastRelays by broadcastViewModel.relays.collectAsStateWithLifecycle() + val indexerRelays by indexerViewModel.relays.collectAsStateWithLifecycle() Scaffold( topBar = { @@ -163,6 +172,7 @@ fun MappedAllRelayListView( trustedViewModel.clear() blockedViewModel.clear() broadcastViewModel.clear() + indexerViewModel.clear() newNav.popBack() }, onPost = { @@ -174,6 +184,7 @@ fun MappedAllRelayListView( trustedViewModel.create() blockedViewModel.create() broadcastViewModel.create() + indexerViewModel.create() newNav.popBack() }, ) @@ -240,6 +251,17 @@ fun MappedAllRelayListView( } renderBroadcastItems(broadcastRelays, broadcastViewModel, accountViewModel, newNav) + item { + SettingsCategoryWithButton( + stringRes(R.string.indexer_section), + stringRes(R.string.indexer_section_explainer), + SettingsCategorySpacingModifier, + ) { + ResetIndexerRelays(indexerViewModel) + } + } + renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, newNav) + item { SettingsCategoryWithButton( stringRes(R.string.search_section), @@ -307,6 +329,23 @@ fun ResetSearchRelays(postViewModel: SearchRelayListViewModel) { } } +@Composable +fun ResetIndexerRelays(postViewModel: IndexerRelayListViewModel) { + OutlinedButton( + onClick = { + postViewModel.deleteAll() + DefaultIndexerRelayList.forEach { + postViewModel.addRelay( + relaySetupInfoBuilder(it), + ) + } + postViewModel.loadRelayDocuments() + }, + ) { + Text(stringRes(R.string.default_relays)) + } +} + @Composable fun ResetDMRelays(postViewModel: DMRelayListViewModel) { OutlinedButton( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt similarity index 100% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/TrustedRelayListView.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/broadcast/BroadcastRelayListView.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt new file mode 100644 index 0000000000..59c2671274 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer + +@Composable +fun IndexerRelayList( + postViewModel: IndexerRelayListViewModel, + accountViewModel: AccountViewModel, + onClose: () -> Unit, + nav: INav, +) { + val feedState by postViewModel.relays.collectAsStateWithLifecycle() + + val newNav = rememberExtendedNav(nav, onClose) + + Row(verticalAlignment = Alignment.CenterVertically) { + LazyColumn( + contentPadding = FeedPadding, + ) { + renderIndexerItems(feedState, postViewModel, accountViewModel, newNav) + } + } +} + +fun LazyListScope.renderIndexerItems( + feedState: List, + postViewModel: IndexerRelayListViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + itemsIndexed(feedState, key = { _, item -> "Indexer" + item.relay }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + accountViewModel = accountViewModel, + nav, + ) + } + + item { + Spacer(modifier = StdVertSpacer) + RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt new file mode 100644 index 0000000000..8bbb81fa2f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class IndexerRelayListViewModel : BasicRelaySetupInfoModel() { + override fun getRelayList(): List? = + account.indexerRelayList.flow.value + .toList() + + override suspend fun saveRelayList(urlList: List) { + account.saveIndexerRelayList(urlList) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index e22eb57ee6..20dc7aebc0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -122,6 +122,7 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplayBlockedRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayBroadcastRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayDMRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList +import com.vitorpamplona.amethyst.ui.note.types.DisplayIndexerRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet @@ -210,6 +211,7 @@ import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -605,6 +607,8 @@ private fun FullBleedNoteCompose( DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is TrustedRelayListEvent) { DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) + } else if (noteEvent is IndexerRelayListEvent) { + DisplayIndexerRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is BroadcastRelayListEvent) { DisplayBroadcastRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is FhirResourceEvent) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 89d142a5f0..82f1b30244 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1094,7 +1094,11 @@ Broadcast Relays Broadcast Relays - Relays that specialize in pushing your notes to all of the other relays. Amethyst will add this relay to all new events made by you + Relays that specialize in pushing your notes to all of the other relays, like sendit.nosflare.com. Amethyst will add this relay to all new events made by you + + Indexer Relays + Indexer Relays + Relays that specialize in hosting everyone\'s metadata and relay lists, like purplepag.es. Amethyst will use these relays to find users that are not in your lists. Blocked Relays Blocked Relays diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 6715c03687..8b73bfbc48 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -83,6 +83,7 @@ import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -233,6 +234,7 @@ class EventFactory { HashtagListEvent.KIND -> HashtagListEvent(id, pubKey, createdAt, tags, content, sig) HighlightEvent.KIND -> HighlightEvent(id, pubKey, createdAt, tags, content, sig) HTTPAuthorizationEvent.KIND -> HTTPAuthorizationEvent(id, pubKey, createdAt, tags, content, sig) + IndexerRelayListEvent.KIND -> IndexerRelayListEvent(id, pubKey, createdAt, tags, content, sig) InteractiveStoryPrologueEvent.KIND -> InteractiveStoryPrologueEvent(id, pubKey, createdAt, tags, content, sig) InteractiveStorySceneEvent.KIND -> InteractiveStorySceneEvent(id, pubKey, createdAt, tags, content, sig) InteractiveStoryReadingStateEvent.KIND -> InteractiveStoryReadingStateEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt index 25c9410c33..5969ca622a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt @@ -79,6 +79,7 @@ class NostrSignerSync( toPublicKey: HexKey, ): String { if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() + if (plaintext.isBlank()) return "" return Nip04.encrypt( plaintext, @@ -102,6 +103,7 @@ class NostrSignerSync( toPublicKey: HexKey, ): String { if (keyPair.privKey == null) throw SignerExceptions.ReadOnlyException() + if (plaintext.isBlank()) return "" return Nip44 .encrypt( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt index f09930df0b..05292c9264 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt @@ -66,7 +66,7 @@ class BroadcastRelayListEvent( fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null) - fun createAddressTag(pubKey: HexKey): String = Address.Companion.assemble(KIND, pubKey, "") + fun createAddressTag(pubKey: HexKey): String = Address.assemble(KIND, pubKey, "") suspend fun updateRelayList( earlierVersion: BroadcastRelayListEvent, @@ -75,8 +75,9 @@ class BroadcastRelayListEvent( createdAt: Long = TimeUtils.now(), ): BroadcastRelayListEvent { val newRelayList = relays.map { RelayTag.assemble(it) } + println("AABBCC 1 ${earlierVersion.content}") val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() - + println("AABBCC 2 ${privateTags.size}") val publicTags = earlierVersion.tags.remove(RelayTag::match) val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt new file mode 100644 index 0000000000..ec057353d0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.relayLists + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.indexerRelays +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relays +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.plus + +@Immutable +class IndexerRelayListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicRelays() = tags.relays() + + suspend fun decryptPrivateRelays(signer: NostrSigner) = privateTags(signer)?.relays() + + suspend fun decryptRelays(signer: NostrSigner): List = publicRelays() + (decryptPrivateRelays(signer) ?: emptyList()) + + companion object { + const val KIND = 10087 + const val ALT = "Broadcasting relays from this author" + val TAGS = arrayOf(AltTag.assemble(ALT)) + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "") + + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null) + + fun createAddressTag(pubKey: HexKey): String = Address.Companion.assemble(KIND, pubKey, "") + + suspend fun updateRelayList( + earlierVersion: IndexerRelayListEvent, + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): IndexerRelayListEvent { + val newRelayList = relays.map { RelayTag.assemble(it) } + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + + val publicTags = earlierVersion.tags.remove(RelayTag::match) + val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) + + return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags) + } + + suspend fun create( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): IndexerRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun create( + relays: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): IndexerRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun build( + publicRelays: List = emptyList(), + privateRelays: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + indexerRelays(publicRelays) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt index c6cb3e2e47..f2d82f1cdf 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.tags.NameTag @@ -42,6 +43,8 @@ fun TagArrayBuilder.trustedRelays(relays: List.broadcastRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) +fun TagArrayBuilder.indexerRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) + fun TagArrayBuilder.privateRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) fun TagArrayBuilder.relaySet(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt index f6d23f26d1..40cf1749c4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip55AndroidSigner/client/NostrSignerExternal.kt @@ -96,6 +96,8 @@ class NostrSignerExternal( plaintext: String, toPublicKey: HexKey, ): String { + if (plaintext.isBlank()) return "" + val result = backgroundQuery.nip04Encrypt(plaintext, toPublicKey) ?: foregroundQuery.nip04Encrypt(plaintext, toPublicKey) if (result is SignerResult.RequestAddressed.Successful) { @@ -124,6 +126,8 @@ class NostrSignerExternal( plaintext: String, toPublicKey: HexKey, ): String { + if (plaintext.isBlank()) return "" + val result = backgroundQuery.nip44Encrypt(plaintext, toPublicKey) ?: foregroundQuery.nip44Encrypt(plaintext, toPublicKey) if (result is SignerResult.RequestAddressed.Successful) { From a494257a27e0659dc209cdc3979de74138481b7f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 23 Jul 2025 19:09:04 -0400 Subject: [PATCH 125/185] Adds Proxy relay list and adds support for fixed list of relays. --- .../vitorpamplona/amethyst/model/Account.kt | 19 ++- .../amethyst/model/LocalCache.kt | 8 ++ ...ys.kt => FollowListOutboxOrProxyRelays.kt} | 93 ++++++++----- .../nip02FollowLists/FollowsPerOutboxRelay.kt | 77 ++++++++--- .../blockedRelays/BlockedRelayListState.kt | 13 +- .../hashtagLists/HashtagListState.kt | 2 +- .../ProxyRelayListDecryptionCache.kt | 29 ++++ .../proxyRelays/ProxyRelayListState.kt | 87 ++++++++++++ .../serverList/MergedFollowListsState.kt | 8 +- .../MergedFollowPlusMineRelayListsState.kt | 15 +- .../topNavFeeds/FeedTopNavFilterState.kt | 9 +- .../MergedTopFeedAuthorListsState.kt | 12 +- .../AllFollowsByOutboxTopNavFilter.kt | 13 +- .../AllFollowsByProxyTopNavFilter.kt | 105 ++++++++++++++ .../allFollows/AllFollowsFeedFlow.kt | 38 ++++-- ...r.kt => AllFollowsTopNavPerRelayFilter.kt} | 2 +- ...t => AllFollowsTopNavPerRelayFilterSet.kt} | 4 +- .../topNavFeeds/aroundMe/AroundMeFeedFlow.kt | 19 ++- .../aroundMe/LocationTopNavFilter.kt | 18 +-- .../topNavFeeds/global/GlobalFeedFlow.kt | 5 +- .../topNavFeeds/global/GlobalTopNavFilter.kt | 23 +++- .../hashtag/HashtagTopNavFilter.kt | 17 +-- .../topNavFeeds/noteBased/NoteFeedFlow.kt | 129 ++++++++++++++++-- .../author/AuthorsByOutboxTopNavFilter.kt | 8 +- .../author/AuthorsByProxyTopNavFilter.kt | 58 ++++++++ ...lter.kt => AuthorsTopNavPerRelayFilter.kt} | 2 +- ...t.kt => AuthorsTopNavPerRelayFilterSet.kt} | 4 +- .../muted/MutedAuthorsByOutboxTopNavFilter.kt | 8 +- .../muted/MutedAuthorsByProxyTopNavFilter.kt | 58 ++++++++ ...kt => MutedAuthorsTopNavPerRelayFilter.kt} | 2 +- ...=> MutedAuthorsTopNavPerRelayFilterSet.kt} | 4 +- .../FilterAccountInfoAndListsFromKey.kt | 6 + .../FilterBasicAccountInfoFromKeys.kt | 6 + .../amethyst/ui/dal/FilterByListParams.kt | 5 +- .../amethyst/ui/note/NoteCompose.kt | 3 + .../amethyst/ui/note/types/RelayList.kt | 21 +++ .../nip23LongForm/SubAssemblyHelper.kt | 12 +- .../subassemblies/FilterLongFormByAuthors.kt | 8 +- .../subassemblies/FilterLongFormByFollows.kt | 4 +- .../RenderPublicChatChannelThumb.kt | 6 + .../discover/nip28Chats/SubAssemblyHelper.kt | 12 +- .../FilterPublicChatsByAuthors.kt | 8 +- .../FilterPublicChatsByFollows.kt | 4 +- .../nip51FollowSets/SubAssemblyHelper.kt | 12 +- .../FilterFollowSetsByAuthors.kt | 8 +- .../FilterFollowSetsByFollows.kt | 4 +- .../DiscoverLiveFeedFilter.kt | 6 + .../nip53LiveActivities/LiveActivityCard.kt | 6 + .../nip53LiveActivities/SubAssemblyHelper.kt | 12 +- .../FilterLiveActivitiesByAuthors.kt | 8 +- .../FilterLiveActivitiesByFollows.kt | 4 +- .../nip72Communities/CommunityCard.kt | 6 + .../nip72Communities/SubAssemblyHelper.kt | 12 +- .../FilterCommunitiesByAuthors.kt | 8 +- .../FilterCommunitiesByFollows.kt | 4 +- .../discover/nip90DVMs/SubAssemblyHelper.kt | 12 +- .../FilterContentDVMsByAuthors.kt | 8 +- .../FilterContentDVMsByFollows.kt | 4 +- .../nip99Classifieds/SubAssemblyHelper.kt | 12 +- .../FilterClassifiedsByAuthors.kt | 8 +- .../FilterClassifiedsByFollows.kt | 4 +- .../screen/loggedIn/geohash/GeoHashScreen.kt | 2 +- .../datasource/FilterPostsByGeohash.kt | 4 +- .../GeoHashFilterAssemblerSubscription.kt | 2 +- .../screen/loggedIn/hashtag/HashtagScreen.kt | 2 +- .../HashtagFilterAssemblerSubscription.kt | 2 +- .../home/dal/HomeConversationsFeedFilter.kt | 5 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 6 + .../home/dal/HomeNewThreadFeedFilter.kt | 5 +- .../FilterHomePostsByAllFollows.kt | 4 +- .../nip65Follows/FilterHomePostsByAuthors.kt | 8 +- .../HomeOutboxEventsEoseManager.kt | 12 +- .../loggedIn/relays/AllRelayListScreen.kt | 19 +++ .../relays/proxy/ProxyRelayListView.kt | 81 +++++++++++ .../relays/proxy/ProxyRelayListViewModel.kt | 34 +++++ .../loggedIn/threadview/ThreadFeedView.kt | 4 + .../VideoOutboxEventsFilterSubAssembler.kt | 12 +- .../FilterPictureAndVideoByAuthors.kt | 8 +- .../FilterPictureAndVideoByFollows.kt | 4 +- amethyst/src/main/res/values/strings.xml | 5 + .../com/vitorpamplona/quartz/EventFactory.kt | 2 + .../relayLists/BlockedRelayListEvent.kt | 2 +- .../relayLists/BroadcastRelayListEvent.kt | 4 +- .../relayLists/IndexerRelayListEvent.kt | 4 +- .../relayLists/ProxyRelayListEvent.kt | 121 ++++++++++++++++ .../relayLists/tags/TagArrayBuilderExt.kt | 3 + 86 files changed, 1184 insertions(+), 293 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/{FollowListOutboxRelays.kt => FollowListOutboxOrProxyRelays.kt} (64%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/{AllFollowsByOutboxTopNavPerRelayFilter.kt => AllFollowsTopNavPerRelayFilter.kt} (97%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/{AllFollowsByOutboxTopNavPerRelayFilterSet.kt => AllFollowsTopNavPerRelayFilterSet.kt} (91%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/{AuthorsByOutboxTopNavPerRelayFilter.kt => AuthorsTopNavPerRelayFilter.kt} (96%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/{AuthorsByOutboxTopNavPerRelayFilterSet.kt => AuthorsTopNavPerRelayFilterSet.kt} (91%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/{MutedAuthorsByOutboxTopNavPerRelayFilter.kt => MutedAuthorsTopNavPerRelayFilter.kt} (96%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/{MutedAuthorsByOutboxTopNavPerRelayFilterSet.kt => MutedAuthorsTopNavPerRelayFilterSet.kt} (91%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/ProxyRelayListEvent.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index bbe9163473..d19d585120 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -34,7 +34,7 @@ import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.NotificationInboxRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.UserMetadataState -import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowsPerOutboxRelay import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState @@ -64,6 +64,8 @@ import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayLis import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.muteList.MuteListState +import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListDecryptionCache +import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache @@ -246,6 +248,9 @@ class Account( val trustedRelayListDecryptionCache = TrustedRelayListDecryptionCache(signer) val trustedRelayList = TrustedRelayListState(signer, cache, trustedRelayListDecryptionCache, scope, settings) + val proxyRelayListDecryptionCache = ProxyRelayListDecryptionCache(signer) + val proxyRelayList = ProxyRelayListState(signer, cache, proxyRelayListDecryptionCache, scope, settings) + val broadcastRelayListDecryptionCache = BroadcastRelayListDecryptionCache(signer) val broadcastRelayList = BroadcastRelayListState(signer, cache, broadcastRelayListDecryptionCache, scope, settings) @@ -297,11 +302,11 @@ class Account( val trustedRelays = TrustedRelayListsState(nip65RelayList, privateStorageRelayList, localRelayList, dmRelayList, searchRelayList, trustedRelayList, broadcastRelayList, scope) // Follows Relays - val followOutboxes = FollowListOutboxRelays(kind3FollowList, blockedRelayList, cache, scope) - val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxes, nip65RelayList, privateStorageRelayList, localRelayList, trustedRelayList, broadcastRelayList, indexerRelayList, scope) + val followOutboxesOrProxy = FollowListOutboxOrProxyRelays(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope) + val followPlusAllMine = MergedFollowPlusMineRelayListsState(followOutboxesOrProxy, nip65RelayList, privateStorageRelayList, localRelayList, broadcastRelayList, indexerRelayList, scope) // keeps a cache of the outbox relays for each author - val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, cache, scope).flow + val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow // Merges all follow lists to create a single All Follows feed. val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope) @@ -341,6 +346,7 @@ class Account( locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, blockedRelays = blockedRelayList.flow, + proxyRelays = proxyRelayList.flow, caches = feedDecryptionCaches, signer = signer, scope = scope, @@ -355,6 +361,7 @@ class Account( locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, blockedRelays = blockedRelayList.flow, + proxyRelays = proxyRelayList.flow, caches = feedDecryptionCaches, signer = signer, scope = scope, @@ -369,6 +376,7 @@ class Account( locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, blockedRelays = blockedRelayList.flow, + proxyRelays = proxyRelayList.flow, caches = feedDecryptionCaches, signer = signer, scope = scope, @@ -383,6 +391,7 @@ class Account( locationFlow = geolocationFlow, followsRelays = followPlusAllMine.flow, blockedRelays = blockedRelayList.flow, + proxyRelays = proxyRelayList.flow, caches = feedDecryptionCaches, signer = signer, scope = scope, @@ -1728,6 +1737,8 @@ class Account( suspend fun saveBroadcastRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(broadcastRelayList.saveRelayList(trustedRelays)) + suspend fun saveProxyRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(proxyRelayList.saveRelayList(trustedRelays)) + suspend fun saveTrustedRelayList(trustedRelays: List) = sendMyPublicAndPrivateOutbox(trustedRelayList.saveRelayList(trustedRelays)) suspend fun saveBlockedRelayList(blockedRelays: List) = sendMyPublicAndPrivateOutbox(blockedRelayList.saveRelayList(blockedRelays)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 3c68f850ee..47580cabf9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -140,6 +140,7 @@ import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -959,6 +960,12 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( + event: ProxyRelayListEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeBaseReplaceable(event, relay, wasVerified) + private fun consume( event: IndexerRelayListEvent, relay: NormalizedRelayUrl?, @@ -2790,6 +2797,7 @@ object LocalCache : ILocalCache { is PictureEvent -> consume(event, relay, wasVerified) is PrivateDmEvent -> consume(event, relay, wasVerified) is PrivateOutboxRelayListEvent -> consume(event, relay, wasVerified) + is ProxyRelayListEvent -> consume(event, relay, wasVerified) is PinListEvent -> consume(event, relay, wasVerified) is PeopleListEvent -> consume(event, relay, wasVerified) is PollNoteEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt similarity index 64% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt index cb577d294e..cf90954fbf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxRelays.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowListOutboxOrProxyRelays.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -31,19 +32,24 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest +import kotlin.collections.flatten -class FollowListOutboxRelays( +class FollowListOutboxOrProxyRelays( kind3Follows: FollowListState, blockedRelayList: BlockedRelayListState, + proxyRelayList: ProxyRelayListState, val cache: LocalCache, scope: CoroutineScope, ) { @@ -57,34 +63,41 @@ class FollowListOutboxRelays( fun allRelayListFlows(followList: Set): List> = followList.map { getNIP65RelayListFlow(it) } - fun combineAllFlows( - flows: List>, - blockedRelays: Set, - ): Flow> = + fun combineAllFlows(flows: List>): Flow> = combine(flows) { relayListNotes: Array -> - relayListNotes.mapNotNull { - (it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() - } - }.map { - it.flatten().toSet().minus(blockedRelays) + relayListNotes + .mapNotNull { + (it.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm() + }.flatten() + .toSet() } @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow> = - combineTransform(kind3Follows.flow, blockedRelayList.flow) { followList, blockedRelays -> - val flows: List> = allRelayListFlows(followList.authors) - val relayListFlows = combineAllFlows(flows, blockedRelays) - emitAll(relayListFlows) + val outboxRelayFlow: StateFlow> = + kind3Follows.flow + .transformLatest { + emitAll(combineAllFlows(allRelayListFlows(it.authors))) + }.onStart { + emit( + kind3Follows.flow.value.authors + .mapNotNull { + getNIP65RelayList(it)?.writeRelaysNorm() + }.flatten() + .toSet(), + ) + }.distinctUntilChanged() + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + val outboxRelayMinusBlockedFlow: StateFlow> = + combine(outboxRelayFlow, blockedRelayList.flow) { followList, blockedRelays -> + followList.minus(blockedRelays) }.onStart { - val blocked = blockedRelayList.flow.value.toSet() - val authors = kind3Follows.flow.value.authors - val perRelay = - authors - .mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm()?.minus(blocked) - }.flatten() - .toSet() - emit(perRelay) + emit(outboxRelayFlow.value.minus(blockedRelayList.flow.value.toSet())) }.flowOn(Dispatchers.Default) .stateIn( scope, @@ -92,19 +105,35 @@ class FollowListOutboxRelays( emptySet(), ) + @OptIn(ExperimentalCoroutinesApi::class) + val flow: StateFlow> = + proxyRelayList.flow + .flatMapLatest { proxyRelays -> + if (proxyRelays.isEmpty()) { + outboxRelayMinusBlockedFlow + } else { + MutableStateFlow(proxyRelays) + } + }.onStart { + emit( + proxyRelayList.flow.value.ifEmpty { + outboxRelayMinusBlockedFlow.value + }, + ) + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + @OptIn(ExperimentalCoroutinesApi::class) val flowSet: StateFlow> = flow .map { relayList -> relayList.map { it.url }.toSet() }.onStart { - emit( - kind3Follows.flow.value.authors - .mapNotNull { - getNIP65RelayList(it)?.writeRelaysNorm()?.map { it.url }?.toSet() - }.flatten() - .toSet(), - ) + emit(flow.value.map { it.url }.toSet()) }.flowOn(Dispatchers.Default) .stateIn( scope, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt index 39ac9db840..4f6fe71288 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip02FollowLists/FollowsPerOutboxRelay.kt @@ -24,6 +24,8 @@ import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState +import com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays.ProxyRelayListState +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @@ -35,15 +37,19 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transformLatest class FollowsPerOutboxRelay( kind3Follows: FollowListState, blockedRelayList: BlockedRelayListState, + proxyRelayList: ProxyRelayListState, val cache: LocalCache, scope: CoroutineScope, ) { @@ -57,18 +63,13 @@ class FollowsPerOutboxRelay( fun allRelayListFlows(followList: Set): List> = followList.map { getNIP65RelayListFlow(it) } - fun combineAllFlows( - flows: List>, - blockedList: Set, - ): Flow>> = + fun combineAllRelayListFlows(flows: List>): Flow>> = combine(flows) { relayListNotes: Array -> mapOfSet { relayListNotes.forEach { noteState -> noteState.note.author?.pubkeyHex?.let { authorHex -> (noteState.note.event as? AdvertisedRelayListEvent)?.writeRelaysNorm()?.forEach { relay -> - if (relay !in blockedList) { - add(relay, authorHex) - } + add(relay, authorHex) } } } @@ -76,27 +77,61 @@ class FollowsPerOutboxRelay( } @OptIn(ExperimentalCoroutinesApi::class) - val flow: StateFlow>> = - combineTransform(kind3Follows.flow, blockedRelayList.flow) { followList, blockedList -> - val flows: List> = allRelayListFlows(followList.authors) - val relayListFlows = combineAllFlows(flows, blockedList) - emitAll(relayListFlows) - }.onStart { - emit( - mapOfSet { - kind3Follows.flow.value.authors.map { authorHex -> - getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay -> - if (relay !in blockedRelayList.flow.value) { + val outboxPerRelayFlow: StateFlow>> = + kind3Follows.flow + .transformLatest { + emitAll(combineAllRelayListFlows(allRelayListFlows(it.authors))) + }.onStart { + emit( + mapOfSet { + kind3Follows.flow.value.authors.map { authorHex -> + getNIP65RelayList(authorHex)?.writeRelaysNorm()?.forEach { relay -> add(relay, authorHex) } } - } - }, + }, + ) + }.distinctUntilChanged() + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyMap(), ) + + val outboxPerRelayMinusBlockedFlow: StateFlow>> = + combine(outboxPerRelayFlow, blockedRelayList.flow) { followList, blockedRelays -> + followList.minus(blockedRelays) + }.onStart { + emit(outboxPerRelayFlow.value.minus(blockedRelayList.flow.value.toSet())) }.flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, emptyMap(), ) + + @OptIn(ExperimentalCoroutinesApi::class) + val flow: StateFlow>> = + proxyRelayList.flow + .flatMapLatest { proxyRelays -> + if (proxyRelays.isEmpty()) { + outboxPerRelayMinusBlockedFlow + } else { + kind3Follows.flow.map { follows -> + proxyRelays.associateWith { follows.authors } + } + } + }.onStart { + if (proxyRelayList.flow.value.isEmpty()) { + emit(outboxPerRelayMinusBlockedFlow.value) + } else { + emit(proxyRelayList.flow.value.associateWith { kind3Follows.flow.value.authors }) + } + }.flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyMap(), + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt index 237e892164..394bfd1bac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt @@ -49,7 +49,7 @@ class BlockedRelayListState( val scope: CoroutineScope, val settings: AccountSettings, ) { - fun getBlockedRelayListAddress() = BlockedRelayListEvent.Companion.createAddress(signer.pubKey) + fun getBlockedRelayListAddress() = BlockedRelayListEvent.createAddress(signer.pubKey) fun getBlockedRelayListNote(): AddressableNote = LocalCache.getOrCreateAddressableNote(getBlockedRelayListAddress()) @@ -64,12 +64,13 @@ class BlockedRelayListState( val flow = getBlockedRelayListFlow() - .map { normalizeBlockedRelayListWithBackup(it.note) } - .onStart { emit(normalizeBlockedRelayListWithBackup(getBlockedRelayListNote())) } + .map { + normalizeBlockedRelayListWithBackup(it.note) + }.onStart { emit(normalizeBlockedRelayListWithBackup(getBlockedRelayListNote())) } .flowOn(Dispatchers.Default) .stateIn( scope, - SharingStarted.Companion.Eagerly, + SharingStarted.Eagerly, emptySet(), ) @@ -78,13 +79,13 @@ class BlockedRelayListState( val relayListForBlocked = getBlockedRelayList() return if (relayListForBlocked != null && relayListForBlocked.tags.isNotEmpty()) { - BlockedRelayListEvent.Companion.updateRelayList( + BlockedRelayListEvent.updateRelayList( earlierVersion = relayListForBlocked, relays = blockedRelays, signer = signer, ) } else { - BlockedRelayListEvent.Companion.create( + BlockedRelayListEvent.create( relays = blockedRelays, signer = signer, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt index adf99d0378..6a73325c59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt @@ -48,7 +48,7 @@ class HashtagListState( val scope: CoroutineScope, val settings: AccountSettings, ) { - fun getHashtagListAddress() = HashtagListEvent.Companion.createAddress(signer.pubKey) + fun getHashtagListAddress() = HashtagListEvent.createAddress(signer.pubKey) fun getHashtagListNote(): AddressableNote = cache.getOrCreateAddressableNote(getHashtagListAddress()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt new file mode 100644 index 0000000000..2f4fff3cdb --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListDecryptionCache.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays + +import com.vitorpamplona.amethyst.model.nip51Lists.relayLists.GenericRelayListCache +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent + +class ProxyRelayListDecryptionCache( + signer: NostrSigner, +) : GenericRelayListCache(signer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt new file mode 100644 index 0000000000..2946990a98 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/proxyRelays/ProxyRelayListState.kt @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip51Lists.proxyRelays + +import com.vitorpamplona.amethyst.model.AccountSettings +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +class ProxyRelayListState( + val signer: NostrSigner, + val cache: LocalCache, + val decryptionCache: ProxyRelayListDecryptionCache, + val scope: CoroutineScope, + val settings: AccountSettings, +) { + fun getProxyRelayListAddress() = ProxyRelayListEvent.createAddress(signer.pubKey) + + fun getProxyRelayListNote(): AddressableNote = cache.getOrCreateAddressableNote(getProxyRelayListAddress()) + + fun getProxyRelayListFlow(): StateFlow = getProxyRelayListNote().flow().metadata.stateFlow + + fun getProxyRelayList(): ProxyRelayListEvent? = getProxyRelayListNote().event as? ProxyRelayListEvent + + suspend fun normalizeProxyRelayListWithBackup(note: Note): Set { + val event = note.event as? ProxyRelayListEvent + return event?.let { decryptionCache.relays(it) } ?: emptySet() + } + + val flow = + getProxyRelayListFlow() + .map { normalizeProxyRelayListWithBackup(it.note) } + .onStart { emit(normalizeProxyRelayListWithBackup(getProxyRelayListNote())) } + .flowOn(Dispatchers.Default) + .stateIn( + scope, + SharingStarted.Eagerly, + emptySet(), + ) + + suspend fun saveRelayList(proxyRelays: List): ProxyRelayListEvent { + val relayListForProxy = getProxyRelayList() + + return if (relayListForProxy != null && relayListForProxy.tags.isNotEmpty()) { + ProxyRelayListEvent.updateRelayList( + earlierVersion = relayListForProxy, + relays = proxyRelays, + signer = signer, + ) + } else { + ProxyRelayListEvent.create( + relays = proxyRelays, + signer = signer, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt index 9d0fed219a..bdcb94ed6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.stateIn import kotlin.collections.map @@ -44,14 +45,14 @@ class MergedFollowListsState( ) { fun mergeLists( kind3: FollowListState.Kind3Follows, - hashtages: Set, + hashtags: Set, geohashes: Set, community: Set, ): FollowListState.Kind3Follows = FollowListState.Kind3Follows( kind3.authors, kind3.authorsPlusMe, - kind3.hashtags + hashtages, + kind3.hashtags + hashtags, kind3.geotags + geohashes, kind3.communities + community.map { it.address.toValue() }, ) @@ -73,7 +74,8 @@ class MergedFollowListsState( communityList.flow.value, ), ) - }.flowOn(Dispatchers.Default) + }.sample(200) + .flowOn(Dispatchers.Default) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt index da18f2185a..2b0fa4a56f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowPlusMineRelayListsState.kt @@ -22,10 +22,9 @@ package com.vitorpamplona.amethyst.model.serverList import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState -import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxRelays +import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListOutboxOrProxyRelays import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.indexerRelays.IndexerRelayListState -import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CoroutineScope @@ -38,11 +37,10 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn class MergedFollowPlusMineRelayListsState( - val followsOutboxRelayList: FollowListOutboxRelays, + val followsOutboxOrProxyRelayList: FollowListOutboxOrProxyRelays, val nip65RelayList: Nip65RelayListState, val privateOutboxRelayList: PrivateStorageRelayListState, val localRelayList: LocalRelayListState, - val trustedRelayList: TrustedRelayListState, val broadcastRelayList: BroadcastRelayListState, val indexerRelayList: IndexerRelayListState, val scope: CoroutineScope, @@ -52,12 +50,11 @@ class MergedFollowPlusMineRelayListsState( val flow: StateFlow> = combine( listOf( - followsOutboxRelayList.flow, + followsOutboxOrProxyRelayList.flow, nip65RelayList.outboxFlow, nip65RelayList.inboxFlow, privateOutboxRelayList.flow, localRelayList.flow, - trustedRelayList.flow, broadcastRelayList.flow, indexerRelayList.flow, ), @@ -66,12 +63,11 @@ class MergedFollowPlusMineRelayListsState( emit( mergeLists( arrayOf( - followsOutboxRelayList.flow.value, + followsOutboxOrProxyRelayList.flow.value, nip65RelayList.outboxFlow.value, nip65RelayList.inboxFlow.value, privateOutboxRelayList.flow.value, localRelayList.flow.value, - trustedRelayList.flow.value, broadcastRelayList.flow.value, indexerRelayList.flow.value, ), @@ -83,12 +79,11 @@ class MergedFollowPlusMineRelayListsState( SharingStarted.Eagerly, mergeLists( arrayOf( - followsOutboxRelayList.flow.value, + followsOutboxOrProxyRelayList.flow.value, nip65RelayList.outboxFlow.value, nip65RelayList.inboxFlow.value, privateOutboxRelayList.flow.value, localRelayList.flow.value, - trustedRelayList.flow.value, broadcastRelayList.flow.value, indexerRelayList.flow.value, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt index 50af84a2be..a825fed969 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/FeedTopNavFilterState.kt @@ -51,19 +51,20 @@ class FeedTopNavFilterState( val locationFlow: StateFlow, val followsRelays: StateFlow>, val blockedRelays: StateFlow>, + val proxyRelays: StateFlow>, val caches: FeedDecryptionCaches, val signer: NostrSigner, val scope: CoroutineScope, ) { fun loadFlowsFor(listName: String): IFeedFlowsType = when (listName) { - GLOBAL_FOLLOWS -> GlobalFeedFlow(followsRelays) - ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays, blockedRelays) - AROUND_ME -> AroundMeFeedFlow(locationFlow, followsRelays) + GLOBAL_FOLLOWS -> GlobalFeedFlow(followsRelays, proxyRelays) + ALL_FOLLOWS -> AllFollowsFeedFlow(allFollows, followsRelays, blockedRelays, proxyRelays) + AROUND_ME -> AroundMeFeedFlow(locationFlow, followsRelays, proxyRelays) else -> { val note = LocalCache.checkGetOrCreateAddressableNote(listName) if (note != null) { - NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays, blockedRelays, caches) + NoteFeedFlow(note.flow().metadata.stateFlow, signer, followsRelays, blockedRelays, proxyRelays, caches) } else { UnknownFeedFlow(listName) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt index 06565733b6..fb860ae395 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/MergedTopFeedAuthorListsState.kt @@ -20,14 +20,14 @@ */ package com.vitorpamplona.amethyst.model.topNavFeeds -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.mapOfSet @@ -50,12 +50,12 @@ class MergedTopFeedAuthorListsState( fun authorList(navFilter: IFeedTopNavPerRelayFilterSet): Map?> = when (navFilter) { is AllCommunitiesTopNavPerRelayFilterSet -> emptyMap() - is AllFollowsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } - is AuthorsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } + is AllFollowsTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } + is AuthorsTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } is GlobalTopNavPerRelayFilterSet -> emptyMap() is HashtagTopNavPerRelayFilterSet -> emptyMap() is LocationTopNavPerRelayFilterSet -> emptyMap() - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } + is MutedAuthorsTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } is SingleCommunityTopNavPerRelayFilterSet -> navFilter.set.mapValues { it.value.authors } else -> emptyMap() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt index 79778a61dc..38c4911e50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavFilter.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlin.collections.associateWith /** * This is a big OR filter on all fields. @@ -78,7 +79,7 @@ class AllFollowsByOutboxTopNavFilter( (communities != null && noteEvent.isTaggedAddressableNotes(communities)) } - override fun toPerRelayFlow(cache: LocalCache): Flow { + override fun toPerRelayFlow(cache: LocalCache): Flow { val authorsPerRelay = if (authors != null) { OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } @@ -95,9 +96,9 @@ class AllFollowsByOutboxTopNavFilter( return combine(authorsPerRelay, communitiesPerRelay, defaultRelays, blockedRelays) { perRelayAuthors, perRelayCommunities, default, blockedRelays -> val allRelays = (perRelayAuthors.keys + perRelayCommunities.keys).filter { it !in blockedRelays }.ifEmpty { default } - AllFollowsByOutboxTopNavPerRelayFilterSet( + AllFollowsTopNavPerRelayFilterSet( allRelays.associateWith { - AllFollowsByOutboxTopNavPerRelayFilter( + AllFollowsTopNavPerRelayFilter( authors = perRelayAuthors[it], hashtags = hashtags, geotags = geotags, @@ -108,7 +109,7 @@ class AllFollowsByOutboxTopNavFilter( } } - override fun startValue(cache: LocalCache): AllFollowsByOutboxTopNavPerRelayFilterSet { + override fun startValue(cache: LocalCache): AllFollowsTopNavPerRelayFilterSet { val authorsPerRelay = if (authors != null) { OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } @@ -124,9 +125,9 @@ class AllFollowsByOutboxTopNavFilter( val allRelays = (authorsPerRelay.keys + communitiesPerRelay.keys).filter { it !in blockedRelays.value }.ifEmpty { defaultRelays.value } - return AllFollowsByOutboxTopNavPerRelayFilterSet( + return AllFollowsTopNavPerRelayFilterSet( allRelays.associateWith { - AllFollowsByOutboxTopNavPerRelayFilter( + AllFollowsTopNavPerRelayFilter( authors = authorsPerRelay[it], hashtags = hashtags, geotags = geotags, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt new file mode 100644 index 0000000000..348c101953 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByProxyTopNavFilter.kt @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableNotes +import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHashes +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHashes +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId +import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlin.collections.associateWith + +/** + * This is a big OR filter on all fields. + */ +@Immutable +class AllFollowsByProxyTopNavFilter( + val authors: Set? = null, + val hashtags: Set? = null, + val geotags: Set? = null, + val communities: Set? = null, + val proxyRelays: Set, +) : IFeedTopNavFilter { + val geotagScopes: Set? = geotags?.mapTo(mutableSetOf()) { GeohashId.Companion.toScope(it) } + val hashtagScopes: Set? = hashtags?.mapTo(mutableSetOf()) { HashtagId.Companion.toScope(it) } + + override fun matchAuthor(pubkey: HexKey): Boolean = authors == null || pubkey in authors + + override fun match(noteEvent: Event): Boolean = + if (noteEvent is LiveActivitiesEvent) { + (authors != null && noteEvent.participantsIntersect(authors)) || + (hashtags != null && noteEvent.isTaggedHashes(hashtags)) || + (geotags != null && noteEvent.isTaggedGeoHashes(geotags)) || + (communities != null && noteEvent.isTaggedAddressableNotes(communities)) + } else if (noteEvent is CommentEvent) { + // ignore follows and checks only the root scope + (authors != null && noteEvent.pubKey in authors) || + (hashtags != null && noteEvent.isTaggedHashes(hashtags)) || + (hashtagScopes != null && noteEvent.isTaggedScopes(hashtagScopes)) || + (geotags != null && noteEvent.isTaggedGeoHashes(geotags)) || + (geotagScopes != null && noteEvent.isTaggedScopes(geotagScopes)) || + (communities != null && noteEvent.isTaggedAddressableNotes(communities)) + } else { + (authors != null && noteEvent.pubKey in authors) || + (hashtags != null && noteEvent.isTaggedHashes(hashtags)) || + (geotags != null && noteEvent.isTaggedGeoHashes(geotags)) || + (communities != null && noteEvent.isTaggedAddressableNotes(communities)) + } + + // forces the use of the Proxy on all connections, replacing the outbox model. + override fun toPerRelayFlow(cache: LocalCache): Flow = + MutableStateFlow( + AllFollowsTopNavPerRelayFilterSet( + proxyRelays.associateWith { + AllFollowsTopNavPerRelayFilter( + authors = authors, + hashtags = hashtags, + geotags = geotags, + communities = communities, + ) + }, + ), + ) + + override fun startValue(cache: LocalCache): AllFollowsTopNavPerRelayFilterSet { + // forces the use of the Proxy on all connections, replacing the outbox model. + return AllFollowsTopNavPerRelayFilterSet( + proxyRelays.associateWith { + AllFollowsTopNavPerRelayFilter( + authors = authors, + hashtags = hashtags, + geotags = geotags, + communities = communities, + ) + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt index 112919801d..f233cea57d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsFeedFlow.kt @@ -26,23 +26,37 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine class AllFollowsFeedFlow( val allFollows: StateFlow, val followsRelays: StateFlow>, val blockedRelays: StateFlow>, + val proxyRelays: StateFlow>, ) : IFeedFlowsType { - fun convert(kind3: FollowListState.Kind3Follows?): AllFollowsByOutboxTopNavFilter = + fun convert( + kind3: FollowListState.Kind3Follows?, + proxyRelays: Set, + ): IFeedTopNavFilter = if (kind3 != null) { - AllFollowsByOutboxTopNavFilter( - authors = kind3.authors, - hashtags = kind3.hashtags, - geotags = kind3.geotags, - communities = kind3.communities, - defaultRelays = followsRelays, - blockedRelays = blockedRelays, - ) + if (proxyRelays.isEmpty()) { + AllFollowsByOutboxTopNavFilter( + authors = kind3.authors, + hashtags = kind3.hashtags, + geotags = kind3.geotags, + communities = kind3.communities, + defaultRelays = followsRelays, + blockedRelays = blockedRelays, + ) + } else { + AllFollowsByProxyTopNavFilter( + authors = kind3.authors, + hashtags = kind3.hashtags, + geotags = kind3.geotags, + communities = kind3.communities, + proxyRelays = proxyRelays, + ) + } } else { AllFollowsByOutboxTopNavFilter( authors = emptySet(), @@ -51,9 +65,9 @@ class AllFollowsFeedFlow( ) } - override fun flow() = allFollows.map(::convert) + override fun flow() = combine(allFollows, proxyRelays, ::convert) - override fun startValue(): AllFollowsByOutboxTopNavFilter = convert(allFollows.value) + override fun startValue(): IFeedTopNavFilter = convert(allFollows.value, proxyRelays.value) override suspend fun startValue(collector: FlowCollector) { collector.emit(startValue()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilter.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt index 49f1aa7e6d..af5595c048 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilter.kt @@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId * This is a big OR filter. */ @Immutable -class AllFollowsByOutboxTopNavPerRelayFilter( +class AllFollowsTopNavPerRelayFilter( val authors: Set? = null, val hashtags: Set? = null, val geotags: Set? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilterSet.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt index 7352e976b6..032b4590cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsByOutboxTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/allFollows/AllFollowsTopNavPerRelayFilterSet.kt @@ -23,6 +23,6 @@ package com.vitorpamplona.amethyst.model.topNavFeeds.allFollows import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -class AllFollowsByOutboxTopNavPerRelayFilterSet( - val set: Map, +class AllFollowsTopNavPerRelayFilterSet( + val set: Map, ) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt index 22ceaa083e..a0c66edc0d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/AroundMeFeedFlow.kt @@ -26,30 +26,35 @@ import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine class AroundMeFeedFlow( val location: StateFlow, - val allFollowRelays: StateFlow>, + val outboxRelays: StateFlow>, + val proxyRelays: StateFlow>, ) : IFeedFlowsType { - fun convert(result: LocationState.LocationResult): LocationTopNavFilter = + fun convert( + result: LocationState.LocationResult, + outboxRelays: Set, + proxyRelays: Set, + ): LocationTopNavFilter = if (result is LocationState.LocationResult.Success) { // 2 neighbors deep = 25x25km LocationTopNavFilter( geotags = compute50kmRange(result.geoHash).toSet(), - relays = allFollowRelays, + relayList = proxyRelays.ifEmpty { outboxRelays }, ) } else { // empty feed until we have a successful geohash LocationTopNavFilter( geotags = emptySet(), - relays = allFollowRelays, + relayList = proxyRelays.ifEmpty { outboxRelays }, ) } - override fun flow() = location.map(::convert) + override fun flow() = combine(location, outboxRelays, proxyRelays, ::convert) - override fun startValue(): LocationTopNavFilter = convert(location.value) + override fun startValue(): LocationTopNavFilter = convert(location.value, outboxRelays.value, proxyRelays.value) override suspend fun startValue(collector: FlowCollector) { collector.emit(startValue()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt index 641f3a521c..6dce8b3cbe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/aroundMe/LocationTopNavFilter.kt @@ -30,15 +30,14 @@ import com.vitorpamplona.quartz.nip01Core.tags.geohash.isTaggedGeoHashes import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.MutableStateFlow @Immutable class LocationTopNavFilter( val geotags: Set, - val relays: StateFlow>, + val relayList: Set, ) : IFeedTopNavFilter { - val geotagScopes: Set = geotags.mapTo(mutableSetOf()) { GeohashId.Companion.toScope(it) } + val geotagScopes: Set = geotags.mapTo(mutableSetOf()) { GeohashId.toScope(it) } override fun matchAuthor(pubkey: HexKey): Boolean = true @@ -54,9 +53,12 @@ class LocationTopNavFilter( } override fun toPerRelayFlow(cache: LocalCache): Flow = - relays.map { - LocationTopNavPerRelayFilterSet(it.associateWith { LocationTopNavPerRelayFilter(geotags) }) - } + MutableStateFlow( + LocationTopNavPerRelayFilterSet(relayList.associateWith { LocationTopNavPerRelayFilter(geotags) }), + ) - override fun startValue(cache: LocalCache): LocationTopNavPerRelayFilterSet = LocationTopNavPerRelayFilterSet(relays.value.associateWith { LocationTopNavPerRelayFilter(geotags) }) + override fun startValue(cache: LocalCache): LocationTopNavPerRelayFilterSet = + LocationTopNavPerRelayFilterSet( + relayList.associateWith { LocationTopNavPerRelayFilter(geotags) }, + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt index ff6301ac19..3aa72e1f88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalFeedFlow.kt @@ -28,9 +28,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class GlobalFeedFlow( - val relays: StateFlow>, + val outboxRelays: StateFlow>, + val proxyRelays: StateFlow>, ) : IFeedFlowsType { - val default = GlobalTopNavFilter(relays) + val default = GlobalTopNavFilter(outboxRelays, proxyRelays) override fun flow() = MutableStateFlow(default) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt index 64a53643c5..5bf4028349 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/global/GlobalTopNavFilter.kt @@ -28,23 +28,32 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine +import kotlin.collections.associateWith +import kotlin.collections.isNotEmpty @Immutable class GlobalTopNavFilter( - val relays: StateFlow>, + val outboxRelays: StateFlow>, + val proxyRelays: StateFlow>, ) : IFeedTopNavFilter { override fun matchAuthor(pubkey: HexKey): Boolean = true override fun match(noteEvent: Event) = true override fun toPerRelayFlow(cache: LocalCache): Flow = - relays.map { - GlobalTopNavPerRelayFilterSet(it.associateWith { GlobalTopNavPerRelayFilter }) + combine(outboxRelays, proxyRelays) { outboxRelays, proxyRelays -> + if (proxyRelays.isNotEmpty()) { + GlobalTopNavPerRelayFilterSet(proxyRelays.associateWith { GlobalTopNavPerRelayFilter }) + } else { + GlobalTopNavPerRelayFilterSet(outboxRelays.associateWith { GlobalTopNavPerRelayFilter }) + } } override fun startValue(cache: LocalCache): GlobalTopNavPerRelayFilterSet = - GlobalTopNavPerRelayFilterSet( - relays.value.associateWith { GlobalTopNavPerRelayFilter }, - ) + if (proxyRelays.value.isNotEmpty()) { + GlobalTopNavPerRelayFilterSet(proxyRelays.value.associateWith { GlobalTopNavPerRelayFilter }) + } else { + GlobalTopNavPerRelayFilterSet(outboxRelays.value.associateWith { GlobalTopNavPerRelayFilter }) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt index d13b66bed9..47235adc20 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/hashtag/HashtagTopNavFilter.kt @@ -30,13 +30,12 @@ import com.vitorpamplona.quartz.nip01Core.tags.hashtags.isTaggedHashes import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.MutableStateFlow @Immutable class HashtagTopNavFilter( val hashtags: Set, - val relays: StateFlow>, + val relayList: Set, ) : IFeedTopNavFilter { val hashtagScopes: Set = hashtags.mapTo(mutableSetOf()) { HashtagId.toScope(it) } @@ -50,16 +49,14 @@ class HashtagTopNavFilter( } override fun toPerRelayFlow(cache: LocalCache): Flow = - relays.map { + MutableStateFlow( HashtagTopNavPerRelayFilterSet( - it.associateWith { HashtagTopNavPerRelayFilter(hashtags) }, - ) - } + relayList.associateWith { HashtagTopNavPerRelayFilter(hashtags) }, + ), + ) override fun startValue(cache: LocalCache): HashtagTopNavPerRelayFilterSet = HashtagTopNavPerRelayFilterSet( - relays.value.associateWith { - HashtagTopNavPerRelayFilter(hashtags) - }, + relayList.associateWith { HashtagTopNavPerRelayFilter(hashtags) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt index e2dc5baa92..1bd864bb43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/NoteFeedFlow.kt @@ -28,8 +28,10 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavFilte import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner @@ -43,16 +45,20 @@ import com.vitorpamplona.quartz.nip72ModCommunities.follow.CommunityListEvent import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.flow.combineTransform class NoteFeedFlow( val metadataFlow: StateFlow, val signer: NostrSigner, - val allFollowRelays: StateFlow>, + val outboxRelays: StateFlow>, val blockedRelays: StateFlow>, + val proxyRelays: StateFlow>, val caches: FeedDecryptionCaches, ) : IFeedFlowsType { - fun process(noteEvent: Event): IFeedTopNavFilter = + fun processByOutbox( + noteEvent: Event, + outboxRelays: Set, + ): IFeedTopNavFilter = when (noteEvent) { is PeopleListEvent -> { if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { @@ -71,10 +77,10 @@ class NoteFeedFlow( AllCommunitiesTopNavFilter(caches.communityListCache.cachedCommunityIdSet(noteEvent), blockedRelays) } is HashtagListEvent -> { - HashtagTopNavFilter(caches.hashtagCache.cachedHashtags(noteEvent), allFollowRelays) + HashtagTopNavFilter(caches.hashtagCache.cachedHashtags(noteEvent), outboxRelays) } is GeohashListEvent -> { - LocationTopNavFilter(caches.geohashCache.cachedGeohashes(noteEvent), allFollowRelays) + LocationTopNavFilter(caches.geohashCache.cachedGeohashes(noteEvent), outboxRelays) } is CommunityDefinitionEvent -> { SingleCommunityTopNavFilter( @@ -87,7 +93,10 @@ class NoteFeedFlow( else -> AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays) } - suspend fun FlowCollector.process(noteEvent: Event) { + suspend fun FlowCollector.processByOutbox( + noteEvent: Event, + outboxRelays: Set, + ) { when (noteEvent) { is PeopleListEvent -> { if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { @@ -106,10 +115,10 @@ class NoteFeedFlow( emit(AllCommunitiesTopNavFilter(caches.communityListCache.communityIdSet(noteEvent), blockedRelays)) } is HashtagListEvent -> { - emit(HashtagTopNavFilter(caches.hashtagCache.hashtags(noteEvent), allFollowRelays)) + emit(HashtagTopNavFilter(caches.hashtagCache.hashtags(noteEvent), outboxRelays)) } is GeohashListEvent -> { - emit(LocationTopNavFilter(caches.geohashCache.geohashes(noteEvent), allFollowRelays)) + emit(LocationTopNavFilter(caches.geohashCache.geohashes(noteEvent), outboxRelays)) } is CommunityDefinitionEvent -> { emit( @@ -128,23 +137,113 @@ class NoteFeedFlow( } } + fun processByProxy( + noteEvent: Event, + proxyRelays: Set, + ): IFeedTopNavFilter = + when (noteEvent) { + is PeopleListEvent -> { + if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { + MutedAuthorsByProxyTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), proxyRelays) + } else { + AuthorsByProxyTopNavFilter(caches.peopleListCache.cachedUserIdSet(noteEvent), proxyRelays) + } + } + is MuteListEvent -> { + MutedAuthorsByProxyTopNavFilter(caches.muteListCache.cachedUserIdSet(noteEvent), proxyRelays) + } + is FollowListEvent -> { + AuthorsByProxyTopNavFilter(noteEvent.followIdSet(), proxyRelays) + } + is CommunityListEvent -> { + AllCommunitiesTopNavFilter(caches.communityListCache.cachedCommunityIdSet(noteEvent), blockedRelays) + } + is HashtagListEvent -> { + HashtagTopNavFilter(caches.hashtagCache.cachedHashtags(noteEvent), proxyRelays) + } + is GeohashListEvent -> { + LocationTopNavFilter(caches.geohashCache.cachedGeohashes(noteEvent), proxyRelays) + } + is CommunityDefinitionEvent -> { + SingleCommunityTopNavFilter( + community = noteEvent.addressTag(), + authors = noteEvent.moderatorKeys().toSet().ifEmpty { null }, + relays = noteEvent.relayUrls().toSet(), + blockedRelays = blockedRelays, + ) + } + else -> AuthorsByProxyTopNavFilter(emptySet(), proxyRelays) + } + + suspend fun FlowCollector.processByProxy( + noteEvent: Event, + proxyRelays: Set, + ) { + when (noteEvent) { + is PeopleListEvent -> { + if (noteEvent.dTag() == PeopleListEvent.Companion.BLOCK_LIST_D_TAG) { + emit(MutedAuthorsByProxyTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), proxyRelays)) + } else { + emit(AuthorsByProxyTopNavFilter(caches.peopleListCache.userIdSet(noteEvent), proxyRelays)) + } + } + is MuteListEvent -> { + emit(MutedAuthorsByProxyTopNavFilter(caches.muteListCache.mutedUserIdSet(noteEvent), proxyRelays)) + } + is FollowListEvent -> { + emit(AuthorsByProxyTopNavFilter(noteEvent.followIdSet(), proxyRelays)) + } + is CommunityListEvent -> { + emit(AllCommunitiesTopNavFilter(caches.communityListCache.communityIdSet(noteEvent), blockedRelays)) + } + is HashtagListEvent -> { + emit(HashtagTopNavFilter(caches.hashtagCache.hashtags(noteEvent), proxyRelays)) + } + is GeohashListEvent -> { + emit(LocationTopNavFilter(caches.geohashCache.geohashes(noteEvent), proxyRelays)) + } + is CommunityDefinitionEvent -> { + emit( + SingleCommunityTopNavFilter( + community = noteEvent.addressTag(), + authors = noteEvent.moderatorKeys().toSet().ifEmpty { null }, + relays = noteEvent.relayUrls().toSet(), + blockedRelays = blockedRelays, + ), + ) + } + else -> + emit( + AuthorsByProxyTopNavFilter(emptySet(), proxyRelays), + ) + } + } + @OptIn(ExperimentalCoroutinesApi::class) override fun flow() = - metadataFlow.transformLatest { noteState -> + combineTransform(metadataFlow, outboxRelays, proxyRelays) { noteState, outboxRelays, proxyRelays -> val noteEvent = noteState?.note?.event if (noteEvent == null) { AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays) } else { - process(noteEvent) + if (proxyRelays.isEmpty()) { + processByOutbox(noteEvent, outboxRelays) + } else { + processByProxy(noteEvent, proxyRelays) + } } } override fun startValue(): IFeedTopNavFilter { val noteEvent = metadataFlow.value?.note?.event - if (noteEvent == null) { + return if (noteEvent == null) { return AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays) } else { - return process(noteEvent) + if (proxyRelays.value.isEmpty()) { + processByOutbox(noteEvent, outboxRelays.value) + } else { + processByProxy(noteEvent, proxyRelays.value) + } } } @@ -153,7 +252,11 @@ class NoteFeedFlow( if (noteEvent == null) { collector.emit(AuthorsByOutboxTopNavFilter(emptySet(), blockedRelays)) } else { - collector.process(noteEvent) + if (proxyRelays.value.isEmpty()) { + collector.processByOutbox(noteEvent, outboxRelays.value) + } else { + collector.processByProxy(noteEvent, proxyRelays.value) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt index 1b65e529fa..63dc77fdbd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavFilter.kt @@ -47,11 +47,11 @@ class AuthorsByOutboxTopNavFilter( } fun convert(map: Map>) = - AuthorsByOutboxTopNavPerRelayFilterSet( - map.mapValues { AuthorsByOutboxTopNavPerRelayFilter(it.value) }, + AuthorsTopNavPerRelayFilterSet( + map.mapValues { AuthorsTopNavPerRelayFilter(it.value) }, ) - override fun toPerRelayFlow(cache: LocalCache): Flow { + override fun toPerRelayFlow(cache: LocalCache): Flow { val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } return combine(authorsPerRelay, blockedRelays) { authors, blocked -> @@ -59,7 +59,7 @@ class AuthorsByOutboxTopNavFilter( } } - override fun startValue(cache: LocalCache): AuthorsByOutboxTopNavPerRelayFilterSet { + override fun startValue(cache: LocalCache): AuthorsTopNavPerRelayFilterSet { val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } return convert(authorsPerRelay.minus(blockedRelays.value)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt new file mode 100644 index 0000000000..4489c55eec --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByProxyTopNavFilter.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +@Immutable +class AuthorsByProxyTopNavFilter( + val authors: Set, + val proxyRelays: Set, +) : IFeedTopNavFilter { + override fun matchAuthor(pubkey: HexKey) = pubkey in authors + + override fun match(noteEvent: Event): Boolean = + if (noteEvent is LiveActivitiesEvent) { + noteEvent.participantsIntersect(authors) + } else { + noteEvent.pubKey in authors + } + + override fun toPerRelayFlow(cache: LocalCache): Flow = + MutableStateFlow( + AuthorsTopNavPerRelayFilterSet( + proxyRelays.associateWith { AuthorsTopNavPerRelayFilter(authors) }, + ), + ) + + override fun startValue(cache: LocalCache): AuthorsTopNavPerRelayFilterSet = + AuthorsTopNavPerRelayFilterSet( + proxyRelays.associateWith { AuthorsTopNavPerRelayFilter(authors) }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilter.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt index 1c1d215ea3..abd72b1ec5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilter.kt @@ -24,6 +24,6 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter @Immutable -class AuthorsByOutboxTopNavPerRelayFilter( +class AuthorsTopNavPerRelayFilter( val authors: Set, ) : IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilterSet.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt index 4564dc5fbd..8809e74964 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsByOutboxTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/author/AuthorsTopNavPerRelayFilterSet.kt @@ -23,6 +23,6 @@ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -class AuthorsByOutboxTopNavPerRelayFilterSet( - val set: Map, +class AuthorsTopNavPerRelayFilterSet( + val set: Map, ) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt index d1bd98be0c..b2a5b68ea5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavFilter.kt @@ -47,11 +47,11 @@ class MutedAuthorsByOutboxTopNavFilter( } fun convert(map: Map>) = - MutedAuthorsByOutboxTopNavPerRelayFilterSet( - map.mapValues { MutedAuthorsByOutboxTopNavPerRelayFilter(it.value) }, + MutedAuthorsTopNavPerRelayFilterSet( + map.mapValues { MutedAuthorsTopNavPerRelayFilter(it.value) }, ) - override fun toPerRelayFlow(cache: LocalCache): Flow { + override fun toPerRelayFlow(cache: LocalCache): Flow { val authorsPerRelay = OutboxRelayLoader.toAuthorsPerRelayFlow(authors, cache) { it } return combine(authorsPerRelay, blockedRelays) { authors, blocked -> @@ -59,7 +59,7 @@ class MutedAuthorsByOutboxTopNavFilter( } } - override fun startValue(cache: LocalCache): MutedAuthorsByOutboxTopNavPerRelayFilterSet { + override fun startValue(cache: LocalCache): MutedAuthorsTopNavPerRelayFilterSet { val authorsPerRelay = OutboxRelayLoader.authorsPerRelaySnapshot(authors, cache) { it } return convert(authorsPerRelay.minus(blockedRelays.value)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt new file mode 100644 index 0000000000..36eea59a9d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByProxyTopNavFilter.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +@Immutable +class MutedAuthorsByProxyTopNavFilter( + val authors: Set, + val proxyRelays: Set, +) : IFeedTopNavFilter { + override fun matchAuthor(pubkey: HexKey) = pubkey in authors + + override fun match(noteEvent: Event): Boolean = + if (noteEvent is LiveActivitiesEvent) { + noteEvent.participantsIntersect(authors) + } else { + noteEvent.pubKey in authors + } + + override fun toPerRelayFlow(cache: LocalCache): Flow = + MutableStateFlow( + MutedAuthorsTopNavPerRelayFilterSet( + proxyRelays.associateWith { MutedAuthorsTopNavPerRelayFilter(authors) }, + ), + ) + + override fun startValue(cache: LocalCache): MutedAuthorsTopNavPerRelayFilterSet = + MutedAuthorsTopNavPerRelayFilterSet( + proxyRelays.associateWith { MutedAuthorsTopNavPerRelayFilter(authors) }, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilter.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt index 1ee39cc473..b56f83feba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilter.kt @@ -24,6 +24,6 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilter @Immutable -class MutedAuthorsByOutboxTopNavPerRelayFilter( +class MutedAuthorsTopNavPerRelayFilter( val authors: Set, ) : IFeedTopNavPerRelayFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilterSet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt similarity index 91% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilterSet.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt index e2760be428..69d4c1029b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsByOutboxTopNavPerRelayFilterSet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/topNavFeeds/noteBased/muted/MutedAuthorsTopNavPerRelayFilterSet.kt @@ -23,6 +23,6 @@ package com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -class MutedAuthorsByOutboxTopNavPerRelayFilterSet( - val set: Map, +class MutedAuthorsTopNavPerRelayFilterSet( + val set: Map, ) : IFeedTopNavPerRelayFilterSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt index 099e3d677d..40aed99a84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterAccountInfoAndListsFromKey.kt @@ -31,9 +31,12 @@ import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip37Drafts.privateOutbox.PrivateOutboxRelayListEvent import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent @@ -59,6 +62,9 @@ val AccountInfoAndListsFromKeyKinds2 = TrustedRelayListEvent.KIND, BroadcastRelayListEvent.KIND, IndexerRelayListEvent.KIND, + ProxyRelayListEvent.KIND, + HashtagListEvent.KIND, + GeohashListEvent.KIND, ) val AmethystMetadataKinds = listOf(AppSpecificDataEvent.KIND) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt index 2861de76fd..6348658b42 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterBasicAccountInfoFromKeys.kt @@ -28,9 +28,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent +import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent @@ -53,6 +56,9 @@ val BasicAccountInfoKinds2 = TrustedRelayListEvent.KIND, BroadcastRelayListEvent.KIND, IndexerRelayListEvent.KIND, + ProxyRelayListEvent.KIND, + HashtagListEvent.KIND, + GeohashListEvent.KIND, ) fun filterBasicAccountInfoFromKeys( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt index 8ab4de50c2..7ecbe13393 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/FilterByListParams.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -62,7 +63,7 @@ class FilterByListParams( fun isGlobal(comingFrom: List) = followLists is GlobalTopNavFilter && - comingFrom.any { followLists.relays.value.contains(it) } + comingFrom.any { followLists.outboxRelays.value.contains(it) } fun match( noteEvent: Event, @@ -89,7 +90,7 @@ class FilterByListParams( hiddenUsers: HiddenUsersState.LiveHiddenUsers, ): FilterByListParams = FilterByListParams( - isHiddenList = followLists is MutedAuthorsByOutboxTopNavFilter, + isHiddenList = followLists is MutedAuthorsByOutboxTopNavFilter || followLists is MutedAuthorsByProxyTopNavFilter, followLists = followLists, hiddenLists = hiddenUsers, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 0f9de6c694..71898328a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList import com.vitorpamplona.amethyst.ui.note.types.DisplayIndexerRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList +import com.vitorpamplona.amethyst.ui.note.types.DisplayProxyRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet import com.vitorpamplona.amethyst.ui.note.types.DisplaySearchRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayTrustedRelayList @@ -191,6 +192,7 @@ import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -659,6 +661,7 @@ private fun RenderNoteRow( is BlockedRelayListEvent -> DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) is TrustedRelayListEvent -> DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) is IndexerRelayListEvent -> DisplayIndexerRelayList(baseNote, backgroundColor, accountViewModel, nav) + is ProxyRelayListEvent -> DisplayProxyRelayList(baseNote, backgroundColor, accountViewModel, nav) is BroadcastRelayListEvent -> DisplayBroadcastRelayList(baseNote, backgroundColor, accountViewModel, nav) is PinListEvent -> RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav) is EmojiPackEvent -> RenderEmojiPack(baseNote, true, backgroundColor, accountViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt index afe8555f12..f38e18a308 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/RelayList.kt @@ -234,6 +234,27 @@ fun DisplayTrustedRelayList( ) } +@Composable +fun DisplayProxyRelayList( + baseNote: Note, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val relays by accountViewModel.account.proxyRelayListDecryptionCache.observeDecryptedRelayList(baseNote).collectAsStateWithLifecycle( + accountViewModel.account.proxyRelayListDecryptionCache.fastStartValueForRelayList(baseNote), + ) + + DisplayRelaySet( + relays, + stringRes(id = R.string.proxy_relays_title), + null, + backgroundColor, + accountViewModel, + nav, + ) +} + @Composable fun DisplayIndexerRelayList( baseNote: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt index 6e858f8b69..a6c14d36d5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/SubAssemblyHelper.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies.filterLongFormByAuthors @@ -46,12 +46,12 @@ fun makeLongFormFilter( ): List = when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterLongFormByAllCommunities(feedSettings, since, defaultSince) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterLongFormByFollows(feedSettings, since, defaultSince) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since, defaultSince) + is AllFollowsTopNavPerRelayFilterSet -> filterLongFormByFollows(feedSettings, since, defaultSince) + is AuthorsTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since, defaultSince) is GlobalTopNavPerRelayFilterSet -> filterLongFormGlobal(feedSettings, since, defaultSince) is HashtagTopNavPerRelayFilterSet -> filterLongFormByHashtag(feedSettings, since, defaultSince) is LocationTopNavPerRelayFilterSet -> filterLongFormByGeohash(feedSettings, since, defaultSince) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since, defaultSince) + is MutedAuthorsTopNavPerRelayFilterSet -> filterLongFormByAuthors(feedSettings, since, defaultSince) is SingleCommunityTopNavPerRelayFilterSet -> filterLongFormByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt index eb1b082907..eb2d4311db 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByAuthors.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -50,7 +50,7 @@ fun filterLongFormAuthors( } fun filterLongFormByAuthors( - authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: AuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { @@ -71,7 +71,7 @@ fun filterLongFormByAuthors( } fun filterLongFormByAuthors( - authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: MutedAuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt index b928aad205..1fb64fd407 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormByFollows.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip23LongForm.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterLongFormByFollows( - followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + followsSet: AllFollowsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 53c5f25792..0d4c93a036 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -46,9 +46,12 @@ import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -115,6 +118,9 @@ fun RenderPublicChatChannelThumb( is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors is AllFollowsByOutboxTopNavFilter -> topFilter.authors is SingleCommunityTopNavFilter -> topFilter.authors + is AuthorsByProxyTopNavFilter -> topFilter.authors + is MutedAuthorsByProxyTopNavFilter -> topFilter.authors + is AllFollowsByProxyTopNavFilter -> topFilter.authors else -> null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt index 2425467aa8..3cc60bb857 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/SubAssemblyHelper.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies.filterPublicChatsByAuthors @@ -46,12 +46,12 @@ fun makePublicChatsFilter( ): List = when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterPublicChatsByAllCommunities(feedSettings, since, defaultSince) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByFollows(feedSettings, since, defaultSince) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since, defaultSince) + is AllFollowsTopNavPerRelayFilterSet -> filterPublicChatsByFollows(feedSettings, since, defaultSince) + is AuthorsTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since, defaultSince) is GlobalTopNavPerRelayFilterSet -> filterPublicChatsGlobal(feedSettings, since, defaultSince) is HashtagTopNavPerRelayFilterSet -> filterPublicChatsByHashtag(feedSettings, since, defaultSince) is LocationTopNavPerRelayFilterSet -> filterPublicChatsByGeohash(feedSettings, since, defaultSince) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since, defaultSince) + is MutedAuthorsTopNavPerRelayFilterSet -> filterPublicChatsByAuthors(feedSettings, since, defaultSince) is SingleCommunityTopNavPerRelayFilterSet -> filterPublicChatsByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt index 334502d052..ce6db72046 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByAuthors.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -57,7 +57,7 @@ fun filterPublicChatsAuthors( } fun filterPublicChatsByAuthors( - authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: AuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { @@ -78,7 +78,7 @@ fun filterPublicChatsByAuthors( } fun filterPublicChatsByAuthors( - authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: MutedAuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt index d527dbbfaa..2ee9441872 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsByFollows.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterPublicChatsByFollows( - followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + followsSet: AllFollowsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt index e88f3c0c3a..44cd0f58e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/SubAssemblyHelper.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies.filterFollowSetsByAuthors @@ -46,12 +46,12 @@ fun makeFollowSetsFilter( ): List = when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterFollowSetsByAllCommunities(feedSettings, since, defaultSince) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByFollows(feedSettings, since, defaultSince) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since, defaultSince) + is AllFollowsTopNavPerRelayFilterSet -> filterFollowSetsByFollows(feedSettings, since, defaultSince) + is AuthorsTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since, defaultSince) is GlobalTopNavPerRelayFilterSet -> filterFollowSetsGlobal(feedSettings, since, defaultSince) is HashtagTopNavPerRelayFilterSet -> filterFollowSetsByHashtag(feedSettings, since, defaultSince) is LocationTopNavPerRelayFilterSet -> filterFollowSetsByGeohash(feedSettings, since, defaultSince) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since, defaultSince) + is MutedAuthorsTopNavPerRelayFilterSet -> filterFollowSetsByAuthors(feedSettings, since, defaultSince) is SingleCommunityTopNavPerRelayFilterSet -> filterFollowSetsByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt index f9fed40837..bc1139bc23 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByAuthors.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -50,7 +50,7 @@ fun filterFollowSetsAuthors( } fun filterFollowSetsByAuthors( - authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: AuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { @@ -71,7 +71,7 @@ fun filterFollowSetsByAuthors( } fun filterFollowSetsByAuthors( - authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: MutedAuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt index a7dbfff209..063cfe7867 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsByFollows.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip51FollowSets.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterFollowSetsByFollows( - followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + followsSet: AllFollowsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt index 475b27b894..bf2f1db737 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/DiscoverLiveFeedFilter.kt @@ -25,9 +25,12 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent @@ -80,6 +83,9 @@ open class DiscoverLiveFeedFilter( is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors is AllFollowsByOutboxTopNavFilter -> topFilter.authors is SingleCommunityTopNavFilter -> topFilter.authors + is AuthorsByProxyTopNavFilter -> topFilter.authors + is MutedAuthorsByProxyTopNavFilter -> topFilter.authors + is AllFollowsByProxyTopNavFilter -> topFilter.authors else -> null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt index 7391c8c784..b04f7b8f27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/LiveActivityCard.kt @@ -46,9 +46,12 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteAndMap import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -242,6 +245,9 @@ fun LoadParticipants( is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors is AllFollowsByOutboxTopNavFilter -> topFilter.authors is SingleCommunityTopNavFilter -> topFilter.authors + is AuthorsByProxyTopNavFilter -> topFilter.authors + is MutedAuthorsByProxyTopNavFilter -> topFilter.authors + is AllFollowsByProxyTopNavFilter -> topFilter.authors else -> emptySet() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt index d91a8e598c..16e3cfca26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/SubAssemblyHelper.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies.filterLiveActivitiesByAuthors @@ -46,12 +46,12 @@ fun makeLiveActivitiesFilter( ): List = when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterLiveActivitiesByAllCommunities(feedSettings, since, defaultSince) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByFollows(feedSettings, since, defaultSince) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since, defaultSince) + is AllFollowsTopNavPerRelayFilterSet -> filterLiveActivitiesByFollows(feedSettings, since, defaultSince) + is AuthorsTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since, defaultSince) is GlobalTopNavPerRelayFilterSet -> filterLiveActivitiesGlobal(feedSettings, since, defaultSince) is HashtagTopNavPerRelayFilterSet -> filterLiveActivitiesByHashtag(feedSettings, since, defaultSince) is LocationTopNavPerRelayFilterSet -> filterLiveActivitiesByGeohash(feedSettings, since, defaultSince) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since, defaultSince) + is MutedAuthorsTopNavPerRelayFilterSet -> filterLiveActivitiesByAuthors(feedSettings, since, defaultSince) is SingleCommunityTopNavPerRelayFilterSet -> filterLiveActivitiesByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt index f449224a0d..4f152e76ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByAuthors.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -62,7 +62,7 @@ fun filterLiveActivitiesAuthors( } fun filterLiveActivitiesByAuthors( - authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: AuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { @@ -83,7 +83,7 @@ fun filterLiveActivitiesByAuthors( } fun filterLiveActivitiesByAuthors( - authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: MutedAuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt index 34d3bff28d..c635ff1478 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesByFollows.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip53LiveActivities.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterLiveActivitiesByFollows( - followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + followsSet: AllFollowsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt index 9be21bae56..e1089042f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/CommunityCard.kt @@ -48,9 +48,12 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.ui.layouts.LeftPictureLayout import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -208,6 +211,9 @@ fun LoadModerators( is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors is AllFollowsByOutboxTopNavFilter -> topFilter.authors is SingleCommunityTopNavFilter -> topFilter.authors + is AuthorsByProxyTopNavFilter -> topFilter.authors + is MutedAuthorsByProxyTopNavFilter -> topFilter.authors + is AllFollowsByProxyTopNavFilter -> topFilter.authors else -> null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt index 7d153d2b58..77316bb8f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/SubAssemblyHelper.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies.filterCommunitiesByAuthors @@ -46,12 +46,12 @@ fun makeCommunitiesFilter( ): List = when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterCommunitiesByAllCommunities(feedSettings, since, defaultSince) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByFollows(feedSettings, since, defaultSince) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since, defaultSince) + is AllFollowsTopNavPerRelayFilterSet -> filterCommunitiesByFollows(feedSettings, since, defaultSince) + is AuthorsTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since, defaultSince) is GlobalTopNavPerRelayFilterSet -> filterCommunitiesGlobal(feedSettings, since, defaultSince) is HashtagTopNavPerRelayFilterSet -> filterCommunitiesByHashtag(feedSettings, since, defaultSince) is LocationTopNavPerRelayFilterSet -> filterCommunitiesByGeohash(feedSettings, since, defaultSince) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since, defaultSince) + is MutedAuthorsTopNavPerRelayFilterSet -> filterCommunitiesByAuthors(feedSettings, since, defaultSince) is SingleCommunityTopNavPerRelayFilterSet -> filterCommunitiesByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt index 28abab14d7..934aaba585 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByAuthors.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -51,7 +51,7 @@ fun filterCommunitiesAuthors( } fun filterCommunitiesByAuthors( - authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: AuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { @@ -72,7 +72,7 @@ fun filterCommunitiesByAuthors( } fun filterCommunitiesByAuthors( - authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: MutedAuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt index faa840d474..106dbfe5e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesByFollows.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip72Communities.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterCommunitiesByFollows( - followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + followsSet: AllFollowsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt index 324c1902fe..cd0e1de3c1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/SubAssemblyHelper.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies.filterContentDVMsByAuthors @@ -46,12 +46,12 @@ fun makeContentDVMsFilter( ): List = when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterContentDVMsByAllCommunities(feedSettings, since, defaultSince) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByFollows(feedSettings, since, defaultSince) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since, defaultSince) + is AllFollowsTopNavPerRelayFilterSet -> filterContentDVMsByFollows(feedSettings, since, defaultSince) + is AuthorsTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since, defaultSince) is GlobalTopNavPerRelayFilterSet -> filterContentDVMsGlobal(feedSettings, since, defaultSince) is HashtagTopNavPerRelayFilterSet -> filterContentDVMsByHashtag(feedSettings, since, defaultSince) is LocationTopNavPerRelayFilterSet -> filterContentDVMsByGeohash(feedSettings, since, defaultSince) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since, defaultSince) + is MutedAuthorsTopNavPerRelayFilterSet -> filterContentDVMsByAuthors(feedSettings, since, defaultSince) is SingleCommunityTopNavPerRelayFilterSet -> filterContentDVMsByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt index 7ede517e3c..657367a957 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByAuthors.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -52,7 +52,7 @@ fun filterContentDVMsAuthors( } fun filterContentDVMsByAuthors( - authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: AuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { @@ -73,7 +73,7 @@ fun filterContentDVMsByAuthors( } fun filterContentDVMsByAuthors( - authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: MutedAuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt index 3ea718586c..818c7401d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip90DVMs/subassemblies/FilterContentDVMsByFollows.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip90DVMs.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterContentDVMsByFollows( - followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + followsSet: AllFollowsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt index 91928399ea..49ca8ca0eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/SubAssemblyHelper.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByAllCommunities import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies.filterClassifiedsByAuthors @@ -46,12 +46,12 @@ fun makeClassifiedsFilter( ): List = when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterClassifiedsByAllCommunities(feedSettings, since, defaultSince) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByFollows(feedSettings, since, defaultSince) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since, defaultSince) + is AllFollowsTopNavPerRelayFilterSet -> filterClassifiedsByFollows(feedSettings, since, defaultSince) + is AuthorsTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since, defaultSince) is GlobalTopNavPerRelayFilterSet -> filterClassifiedsGlobal(feedSettings, since, defaultSince) is HashtagTopNavPerRelayFilterSet -> filterClassifiedsByHashtag(feedSettings, since, defaultSince) is LocationTopNavPerRelayFilterSet -> filterClassifiedsByGeohash(feedSettings, since, defaultSince) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since, defaultSince) + is MutedAuthorsTopNavPerRelayFilterSet -> filterClassifiedsByAuthors(feedSettings, since, defaultSince) is SingleCommunityTopNavPerRelayFilterSet -> filterClassifiedsByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt index 034852524e..0b999913a9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -50,7 +50,7 @@ fun filterClassifiedsAuthors( } fun filterClassifiedsByAuthors( - authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: AuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { @@ -71,7 +71,7 @@ fun filterClassifiedsByAuthors( } fun filterClassifiedsByAuthors( - authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: MutedAuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt index b3d8c2554f..7bbf7535e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByFollows.kt @@ -20,12 +20,12 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.subassemblies -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterClassifiedsByFollows( - followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + followsSet: AllFollowsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt index 03877e6942..443f99376e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/GeoHashScreen.kt @@ -68,7 +68,7 @@ fun PrepareViewModelsGeoHashScreen( factory = GeoHashFeedViewModel.Factory( tag.geohash, - accountViewModel.account.followOutboxes.flow.value, + accountViewModel.account.followOutboxesOrProxy.flow.value, accountViewModel.account, ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt index 8df6e09d09..6903974de0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/FilterPostsByGeohash.kt @@ -68,7 +68,7 @@ fun filterPostsByGeohash( Filter( tags = geohashesToFollowMap, kinds = PostsByGeohashKinds, - limit = 200, + limit = 100, since = since, ), ), @@ -78,7 +78,7 @@ fun filterPostsByGeohash( Filter( tags = geohashesScoreMap, kinds = CommentKinds, - limit = 200, + limit = 100, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt index bf5fb2db16..49bbf8ef1a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/geohash/datasource/GeoHashFilterAssemblerSubscription.kt @@ -37,7 +37,7 @@ fun GeoHashFilterAssemblerSubscription( // even if they are tracking the same tag. val state = remember(tag) { - GeohashQueryState(tag.geohash, accountViewModel.account.followOutboxes.flow.value) + GeohashQueryState(tag.geohash, accountViewModel.account.followOutboxesOrProxy.flow.value) } KeyDataSourceSubscription(state, accountViewModel.dataSources().geohashes) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt index 409b7246c3..d8fbb9dc2c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/HashtagScreen.kt @@ -73,7 +73,7 @@ fun PrepareViewModelsHashtagScreen( factory = HashtagFeedViewModel.Factory( tag.hashtag, - accountViewModel.account.followOutboxes.flow.value, + accountViewModel.account.followOutboxesOrProxy.flow.value, accountViewModel.account, ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt index 4f36e302d2..1bf1a83872 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/HashtagFilterAssemblerSubscription.kt @@ -35,7 +35,7 @@ fun HashtagFilterAssemblerSubscription( // even if they are tracking the same tag. val state = remember(tag) { - HashtagQueryState(tag.hashtag, accountViewModel.account.followOutboxes.flow.value) + HashtagQueryState(tag.hashtag, accountViewModel.account.followOutboxesOrProxy.flow.value) } KeyDataSourceSubscription(state, accountViewModel.dataSources().hashtags) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt index 81e4ecc14f..5c940a8fa8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams @@ -39,7 +40,9 @@ class HomeConversationsFeedFilter( ) : AdditiveFeedFilter() { override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultHomeFollowList.value - override fun showHiddenKey(): Boolean = account.liveHomeFollowLists.value is MutedAuthorsByOutboxTopNavFilter + override fun showHiddenKey(): Boolean = + account.liveHomeFollowLists.value is MutedAuthorsByOutboxTopNavFilter || + account.liveHomeFollowLists.value is MutedAuthorsByProxyTopNavFilter override fun feed(): List { val filterParams = buildFilterParams(account) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index d15e5a3362..b7e819a57e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -25,9 +25,12 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveComplexFeedFilter import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent @@ -130,6 +133,9 @@ class HomeLiveFilter( is MutedAuthorsByOutboxTopNavFilter -> topFilter.authors is AllFollowsByOutboxTopNavFilter -> topFilter.authors is SingleCommunityTopNavFilter -> topFilter.authors + is AuthorsByProxyTopNavFilter -> topFilter.authors + is MutedAuthorsByProxyTopNavFilter -> topFilter.authors + is AllFollowsByProxyTopNavFilter -> topFilter.authors else -> null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index fd0d0e67e4..e5a0441fe7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams @@ -45,7 +46,9 @@ class HomeNewThreadFeedFilter( ) : AdditiveFeedFilter() { override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultHomeFollowList.value - override fun showHiddenKey(): Boolean = account.liveHomeFollowLists.value is MutedAuthorsByOutboxTopNavFilter + override fun showHiddenKey(): Boolean = + account.liveHomeFollowLists.value is MutedAuthorsByOutboxTopNavFilter || + account.liveHomeFollowLists.value is MutedAuthorsByProxyTopNavFilter fun buildFilterParams(account: Account): FilterByListParams = FilterByListParams.create( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt index 6bd3ceb1ea..8c6a77b459 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAllFollows.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByGeohashes import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip01Core.filterHomePostsByHashtags @@ -29,7 +29,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Commun import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterHomePostsByAllFollows( - followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + followsSet: AllFollowsTopNavPerRelayFilterSet, since: SincePerRelayMap?, sinceBoundaryNew: Long?, sinceBoundaryReply: Long?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt index 6a63ca2ca7..80c915eadb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent @@ -106,7 +106,7 @@ fun filterReplyHomePostsByAuthors( } fun filterHomePostsByAuthors( - authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: AuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, sinceBoundaryNew: Long?, sinceBoundaryReply: Long?, @@ -133,7 +133,7 @@ fun filterHomePostsByAuthors( } fun filterHomePostsByAuthors( - authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: MutedAuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, sinceBoundaryNew: Long?, sinceBoundaryReply: Long?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt index c04031c3f4..10803e6f10 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/HomeOutboxEventsEoseManager.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip65Follows import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeQueryState @@ -60,12 +60,12 @@ class HomeOutboxEventsEoseManager( val repliesSince = key.feedState.homeReplies.lastNoteCreatedAtIfFilled() return when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterHomePostsByAllCommunities(feedSettings, since, newThreadSince) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAllFollows(feedSettings, since, newThreadSince, repliesSince) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAuthors(feedSettings, since, newThreadSince, repliesSince) + is AllFollowsTopNavPerRelayFilterSet -> filterHomePostsByAllFollows(feedSettings, since, newThreadSince, repliesSince) + is AuthorsTopNavPerRelayFilterSet -> filterHomePostsByAuthors(feedSettings, since, newThreadSince, repliesSince) is GlobalTopNavPerRelayFilterSet -> filterHomePostsByGlobal(feedSettings, since, newThreadSince, repliesSince) is HashtagTopNavPerRelayFilterSet -> filterHomePostsByHashtags(feedSettings, since, newThreadSince) is LocationTopNavPerRelayFilterSet -> filterHomePostsByGeohashes(feedSettings, since, newThreadSince) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterHomePostsByAuthors(feedSettings, since, newThreadSince, repliesSince) + is MutedAuthorsTopNavPerRelayFilterSet -> filterHomePostsByAuthors(feedSettings, since, newThreadSince, repliesSince) is SingleCommunityTopNavPerRelayFilterSet -> filterHomePostsByCommunity(feedSettings, since, newThreadSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index f668c96e9a..0f86454ea4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -64,6 +64,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37.renderPrivateO import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.renderNip65HomeItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.renderNip65NotifItems +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.proxy.ProxyRelayListViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.proxy.renderProxyItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.renderSearchItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.trusted.TrustedRelayListViewModel @@ -90,6 +92,7 @@ fun AllRelayListScreen( val connectedViewModel: ConnectedRelayListViewModel = viewModel() val broadcastViewModel: BroadcastRelayListViewModel = viewModel() val indexerViewModel: IndexerRelayListViewModel = viewModel() + val proxyViewModel: ProxyRelayListViewModel = viewModel() dmViewModel.init(accountViewModel) nip65ViewModel.init(accountViewModel) @@ -101,6 +104,7 @@ fun AllRelayListScreen( trustedViewModel.init(accountViewModel) broadcastViewModel.init(accountViewModel) indexerViewModel.init(accountViewModel) + proxyViewModel.init(accountViewModel) LaunchedEffect(accountViewModel) { dmViewModel.load() @@ -113,6 +117,7 @@ fun AllRelayListScreen( trustedViewModel.load() broadcastViewModel.load() indexerViewModel.load() + proxyViewModel.load() } MappedAllRelayListView( @@ -126,6 +131,7 @@ fun AllRelayListScreen( trustedViewModel, broadcastViewModel, indexerViewModel, + proxyViewModel, accountViewModel, nav, ) @@ -144,6 +150,7 @@ fun MappedAllRelayListView( trustedViewModel: TrustedRelayListViewModel, broadcastViewModel: BroadcastRelayListViewModel, indexerViewModel: IndexerRelayListViewModel, + proxyViewModel: ProxyRelayListViewModel, accountViewModel: AccountViewModel, newNav: INav, ) { @@ -158,6 +165,7 @@ fun MappedAllRelayListView( val connectedRelays by connectedViewModel.relays.collectAsStateWithLifecycle() val broadcastRelays by broadcastViewModel.relays.collectAsStateWithLifecycle() val indexerRelays by indexerViewModel.relays.collectAsStateWithLifecycle() + val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle() Scaffold( topBar = { @@ -173,6 +181,7 @@ fun MappedAllRelayListView( blockedViewModel.clear() broadcastViewModel.clear() indexerViewModel.clear() + proxyViewModel.clear() newNav.popBack() }, onPost = { @@ -185,6 +194,7 @@ fun MappedAllRelayListView( blockedViewModel.create() broadcastViewModel.create() indexerViewModel.create() + proxyViewModel.create() newNav.popBack() }, ) @@ -242,6 +252,15 @@ fun MappedAllRelayListView( } renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, newNav) + item { + SettingsCategory( + stringRes(R.string.proxy_section), + stringRes(R.string.proxy_section_explainer), + SettingsCategorySpacingModifier, + ) + } + renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, newNav) + item { SettingsCategory( stringRes(R.string.broadcast_section), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt new file mode 100644 index 0000000000..1580a41150 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.proxy + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer + +@Composable +fun ProxyRelayList( + postViewModel: ProxyRelayListViewModel, + accountViewModel: AccountViewModel, + onClose: () -> Unit, + nav: INav, +) { + val feedState by postViewModel.relays.collectAsStateWithLifecycle() + + val newNav = rememberExtendedNav(nav, onClose) + + Row(verticalAlignment = Alignment.CenterVertically) { + LazyColumn( + contentPadding = FeedPadding, + ) { + renderProxyItems(feedState, postViewModel, accountViewModel, newNav) + } + } +} + +fun LazyListScope.renderProxyItems( + feedState: List, + postViewModel: ProxyRelayListViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + itemsIndexed(feedState, key = { _, item -> "Proxy" + item.relay }) { index, item -> + BasicRelaySetupInfoDialog( + item, + onDelete = { postViewModel.deleteRelay(item) }, + accountViewModel = accountViewModel, + nav, + ) + } + + item { + Spacer(modifier = StdVertSpacer) + RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt new file mode 100644 index 0000000000..5985a0c14d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.proxy + +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +class ProxyRelayListViewModel : BasicRelaySetupInfoModel() { + override fun getRelayList(): List? = + account.proxyRelayList.flow.value + .toList() + + override suspend fun saveRelayList(urlList: List) { + account.saveProxyRelayList(urlList) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 20dc7aebc0..ae82881ca8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -125,6 +125,7 @@ import com.vitorpamplona.amethyst.ui.note.types.DisplayFollowList import com.vitorpamplona.amethyst.ui.note.types.DisplayIndexerRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayNIP65RelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayPeopleList +import com.vitorpamplona.amethyst.ui.note.types.DisplayProxyRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayRelaySet import com.vitorpamplona.amethyst.ui.note.types.DisplaySearchRelayList import com.vitorpamplona.amethyst.ui.note.types.DisplayTrustedRelayList @@ -212,6 +213,7 @@ import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent @@ -605,6 +607,8 @@ private fun FullBleedNoteCompose( DisplaySearchRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is BlockedRelayListEvent) { DisplayBlockedRelayList(baseNote, backgroundColor, accountViewModel, nav) + } else if (noteEvent is ProxyRelayListEvent) { + DisplayProxyRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is TrustedRelayListEvent) { DisplayTrustedRelayList(baseNote, backgroundColor, accountViewModel, nav) } else if (noteEvent is IndexerRelayListEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt index f169dddf01..99714ded78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/VideoOutboxEventsFilterSubAssembler.kt @@ -21,14 +21,14 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.VideoQueryState @@ -61,12 +61,12 @@ class VideoOutboxEventsFilterSubAssembler( val defaultSince = key.feedState.videoFeed.lastNoteCreatedAtWhenFullyLoaded.value return when (feedSettings) { is AllCommunitiesTopNavPerRelayFilterSet -> filterPictureAndVideoByAllCommunities(feedSettings, since, defaultSince) - is AllFollowsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByFollows(feedSettings, since, defaultSince) - is AuthorsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since, defaultSince) + is AllFollowsTopNavPerRelayFilterSet -> filterPictureAndVideoByFollows(feedSettings, since, defaultSince) + is AuthorsTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since, defaultSince) is GlobalTopNavPerRelayFilterSet -> filterPictureAndVideoGlobal(feedSettings, since, defaultSince) is HashtagTopNavPerRelayFilterSet -> filterPictureAndVideoByHashtag(feedSettings, since, defaultSince) is LocationTopNavPerRelayFilterSet -> filterPictureAndVideoByGeohash(feedSettings, since, defaultSince) - is MutedAuthorsByOutboxTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since, defaultSince) + is MutedAuthorsTopNavPerRelayFilterSet -> filterPictureAndVideoByAuthors(feedSettings, since, defaultSince) is SingleCommunityTopNavPerRelayFilterSet -> filterPictureAndVideoByCommunity(feedSettings, since, defaultSince) else -> emptyList() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt index c1080bd306..29dc11fb22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByAuthors.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip65Follows -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavPerRelayFilterSet -import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.LegacyMimeTypeMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.PictureAndVideoKinds @@ -64,7 +64,7 @@ fun filterPictureAndVideoAuthors( } fun filterPictureAndVideoByAuthors( - authorSet: AuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: AuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { @@ -85,7 +85,7 @@ fun filterPictureAndVideoByAuthors( } fun filterPictureAndVideoByAuthors( - authorSet: MutedAuthorsByOutboxTopNavPerRelayFilterSet, + authorSet: MutedAuthorsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt index 885a85db74..368b25f72e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip65Follows/FilterPictureAndVideoByFollows.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip65Follows -import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavPerRelayFilterSet +import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoGeohash import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassemblies.nip01Core.filterPictureAndVideoHashtag @@ -28,7 +28,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.datasource.subassembl import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter fun filterPictureAndVideoByFollows( - followsSet: AllFollowsByOutboxTopNavPerRelayFilterSet, + followsSet: AllFollowsTopNavPerRelayFilterSet, since: SincePerRelayMap?, defaultSince: Long? = null, ): List { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 82f1b30244..40d6c2bf4f 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -494,6 +494,7 @@ Follow List All Follows + Follows via Proxy Around Me Global Mute List @@ -1092,6 +1093,10 @@ Trusted Relays Relays you trust to not need a Tor connection for + Proxy Relays + Proxy Relays + Aggregator relays the app must use to download your feeds from, like filter.nostr.wine. This replaces the outbox model and will make the app connect to only the relays in your lists. + Broadcast Relays Broadcast Relays Relays that specialize in pushing your notes to all of the other relays, like sendit.nosflare.com. Amethyst will add this relay to all new events made by you diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 8b73bfbc48..82e80f2184 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -84,6 +84,7 @@ import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip52Calendar.CalendarDateSlotEvent @@ -262,6 +263,7 @@ class EventFactory { PollNoteEvent.KIND -> PollNoteEvent(id, pubKey, createdAt, tags, content, sig) PrivateDmEvent.KIND -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) PrivateOutboxRelayListEvent.KIND -> PrivateOutboxRelayListEvent(id, pubKey, createdAt, tags, content, sig) + ProxyRelayListEvent.KIND -> ProxyRelayListEvent(id, pubKey, createdAt, tags, content, sig) ReactionEvent.KIND -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) RelationshipStatusEvent.KIND -> RelationshipStatusEvent(id, pubKey, createdAt, tags, content, sig) RelayAuthEvent.KIND -> RelayAuthEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BlockedRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BlockedRelayListEvent.kt index 4a6a88c4f5..3798037bde 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BlockedRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BlockedRelayListEvent.kt @@ -58,7 +58,7 @@ class BlockedRelayListEvent( companion object { const val KIND = 10006 - val ALT = "Blocked relays from this author" + const val ALT = "Blocked relays from this author" val ALT_TAG = arrayOf(AltTag.Companion.assemble(ALT)) fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "") diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt index 05292c9264..8ec9756107 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/BroadcastRelayListEvent.kt @@ -59,7 +59,7 @@ class BroadcastRelayListEvent( companion object { const val KIND = 10088 - const val ALT = "Broadcasting relays from this author" + const val ALT = "Broadcast relays from this author" val TAGS = arrayOf(AltTag.assemble(ALT)) fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "") @@ -75,9 +75,7 @@ class BroadcastRelayListEvent( createdAt: Long = TimeUtils.now(), ): BroadcastRelayListEvent { val newRelayList = relays.map { RelayTag.assemble(it) } - println("AABBCC 1 ${earlierVersion.content}") val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() - println("AABBCC 2 ${privateTags.size}") val publicTags = earlierVersion.tags.remove(RelayTag::match) val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt index ec057353d0..05b2327b67 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/IndexerRelayListEvent.kt @@ -58,8 +58,8 @@ class IndexerRelayListEvent( suspend fun decryptRelays(signer: NostrSigner): List = publicRelays() + (decryptPrivateRelays(signer) ?: emptyList()) companion object { - const val KIND = 10087 - const val ALT = "Broadcasting relays from this author" + const val KIND = 10086 + const val ALT = "Indexer relays from this author" val TAGS = arrayOf(AltTag.assemble(ALT)) fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "") diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/ProxyRelayListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/ProxyRelayListEvent.kt new file mode 100644 index 0000000000..7f48f3fc45 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/ProxyRelayListEvent.kt @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip51Lists.relayLists + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent +import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent +import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.RelayTag +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.proxyRelays +import com.vitorpamplona.quartz.nip51Lists.relayLists.tags.relays +import com.vitorpamplona.quartz.nip51Lists.remove +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.collections.plus + +@Immutable +class ProxyRelayListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun publicRelays() = tags.relays() + + suspend fun decryptPrivateRelays(signer: NostrSigner) = privateTags(signer)?.relays() + + suspend fun decryptRelays(signer: NostrSigner): List = publicRelays() + (decryptPrivateRelays(signer) ?: emptyList()) + + companion object { + const val KIND = 10087 + const val ALT = "Proxy relays from this author" + val TAGS = arrayOf(AltTag.assemble(ALT)) + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "") + + fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null) + + fun createAddressTag(pubKey: HexKey): String = Address.Companion.assemble(KIND, pubKey, "") + + suspend fun updateRelayList( + earlierVersion: ProxyRelayListEvent, + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): ProxyRelayListEvent { + val newRelayList = relays.map { RelayTag.assemble(it) } + val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException() + + val publicTags = earlierVersion.tags.remove(RelayTag::match) + val newPrivateTags = privateTags.remove(RelayTag::notMatch).plus(newRelayList) + + return signer.signNip51List(createdAt, KIND, publicTags, newPrivateTags) + } + + suspend fun create( + relays: List, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): ProxyRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun create( + relays: List, + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): ProxyRelayListEvent { + val privateTagArray = relays.map { RelayTag.assemble(it) }.toTypedArray() + return signer.signNip51List(createdAt, KIND, TAGS, privateTagArray) + } + + suspend fun build( + publicRelays: List = emptyList(), + privateRelays: List = emptyList(), + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate( + kind = KIND, + description = PrivateTagsInContent.encryptNip44(privateRelays.map { RelayTag.assemble(it) }.toTypedArray(), signer), + createdAt = createdAt, + ) { + alt(ALT) + proxyRelays(publicRelays) + + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt index f2d82f1cdf..a2ce226df0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/relayLists/tags/TagArrayBuilderExt.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BlockedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.BroadcastRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.IndexerRelayListEvent +import com.vitorpamplona.quartz.nip51Lists.relayLists.ProxyRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relayLists.TrustedRelayListEvent import com.vitorpamplona.quartz.nip51Lists.relaySets.RelaySetEvent import com.vitorpamplona.quartz.nip51Lists.tags.NameTag @@ -45,6 +46,8 @@ fun TagArrayBuilder.broadcastRelays(relays: List.indexerRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) +fun TagArrayBuilder.proxyRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) + fun TagArrayBuilder.privateRelays(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) fun TagArrayBuilder.relaySet(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) From 14055894dc5da1977d6cd9cb5745b939dc8e5b24 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 24 Jul 2025 09:22:24 -0400 Subject: [PATCH 126/185] Refactoring in the relay url normalizer --- .../relay/normalizer/RelayUrlNormalizer.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index 5d24e224d2..40a0e51c2d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -46,6 +46,12 @@ class RelayUrlNormalizer { fun isRelaySchemePrefixInsecure(url: String) = url[2] == ':' && url[3] == '/' && url[4] == '/' && url[5] != '/' + fun isHttpPrefix(url: String) = url.length > 8 && url[0] == 'h' && url[1] == 't' && url[2] == 't' && url[3] == 'p' + + fun isHttpSSuffix(url: String) = url[4] == 's' && url[5] == ':' && url[6] == '/' && url[7] == '/' + + fun isHttpSuffix(url: String) = url[4] == ':' && url[5] == '/' && url[6] == '/' + fun isRelayUrl(url: String): Boolean { val trimmed = url.trim().ifEmpty { return false } @@ -72,7 +78,7 @@ class RelayUrlNormalizer { @OptIn(ExperimentalContracts::class) fun fix(url: String): String? { - val trimmed = url.trim().ifEmpty { return null } + val trimmed = url.trim() if (trimmed.isEmpty()) return null @@ -84,11 +90,11 @@ class RelayUrlNormalizer { } // fast for good https:// urls - if (trimmed.length > 8 && trimmed[0] == 'h' && trimmed[1] == 't' && trimmed[2] == 't' && trimmed[3] == 'p') { - if (trimmed[4] == 's' && trimmed[5] == ':' && trimmed[6] == '/' && trimmed[7] == '/') { + if (isHttpPrefix(trimmed)) { + if (isHttpSSuffix(trimmed)) { // https:// return "wss://${trimmed.drop(8)}" - } else if (trimmed[4] == ':' && trimmed[5] == '/' && trimmed[6] == '/') { + } else if (isHttpSuffix(trimmed)) { // http:// return "ws://${trimmed.drop(7)}" } From e76bffd04853e7632152cdc5bdfe2d700a145b36 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 24 Jul 2025 09:37:40 -0400 Subject: [PATCH 127/185] Updates coil, compose, firebase, jackson, lifecyle, mockk, navigation --- gradle/libs.versions.toml | 14 +++++++------- quartz/build.gradle | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b33c950c25..2238a677fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,14 +12,14 @@ audiowaveform = "1.1.2" benchmark = "1.3.4" benchmarkJunit4 = "1.3.4" biometricKtx = "1.2.0-alpha05" -coil = "3.2.0" -composeBom = "2025.06.01" +coil = "3.3.0" +composeBom = "2025.07.00" coreKtx = "1.16.0" espressoCore = "3.6.1" -firebaseBom = "33.16.0" +firebaseBom = "34.0.0" fragmentKtx = "1.8.8" gms = "4.4.3" -jacksonModuleKotlin = "2.19.1" +jacksonModuleKotlin = "2.19.2" jna = "5.17.0" jtorctl = "0.4.5.7" junit = "4.13.2" @@ -29,13 +29,13 @@ kotlinxSerialization = "1.9.0" kotlinxSerializationPlugin = "2.2.0" languageId = "17.0.6" lazysodiumAndroid = "5.2.0" -lifecycleRuntimeKtx = "2.9.1" +lifecycleRuntimeKtx = "2.9.2" lightcompressor = "1.3.3" markdown = "e1151c8" media3 = "1.7.1" -mockk = "1.14.4" +mockk = "1.14.5" kotlinx-coroutines-test = "1.10.2" -navigationCompose = "2.9.1" +navigationCompose = "2.9.2" okhttp = "5.1.0" runner = "1.6.2" rfc3986 = "0.1.2" diff --git a/quartz/build.gradle b/quartz/build.gradle index 0a66790229..2b584d077b 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -57,7 +57,7 @@ dependencies { // LibSodium for ChaCha encryption (NIP-44) // Wait for @aar support in version catalogs - implementation "com.goterl:lazysodium-android:5.1.0@aar" + implementation 'com.goterl:lazysodium-android:5.2.0@aar' implementation 'net.java.dev.jna:jna:5.17.0@aar' //implementation (libs.lazysodium.android) { artifact { type = "aar" } } From 25e92666e0cdf7f1eb7f32a239efa6e1eeed9500 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 24 Jul 2025 09:53:00 -0400 Subject: [PATCH 128/185] Adjusts the gradle DSL language to the latest deprecations --- amethyst/build.gradle | 30 +++++++++++++++--------------- ammolite/build.gradle | 12 ++++++------ benchmark/build.gradle | 12 ++++++------ commons/build.gradle | 12 ++++++------ quartz/build.gradle | 12 ++++++------ settings.gradle | 6 +++--- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index bb29832c2a..0c1e6af165 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -9,20 +9,20 @@ plugins { } android { - namespace 'com.vitorpamplona.amethyst' - compileSdk libs.versions.android.compileSdk.get().toInteger() + namespace = 'com.vitorpamplona.amethyst' + compileSdk = libs.versions.android.compileSdk.get().toInteger() defaultConfig { - applicationId "com.vitorpamplona.amethyst" - minSdk libs.versions.android.minSdk.get().toInteger() - targetSdk libs.versions.android.targetSdk.get().toInteger() - versionCode 418 - versionName "0.94.3" + applicationId = "com.vitorpamplona.amethyst" + minSdk = libs.versions.android.minSdk.get().toInteger() + targetSdk = libs.versions.android.targetSdk.get().toInteger() + versionCode = 418 + versionName = "0.94.3" buildConfigField "String", "RELEASE_NOTES_ID", "\"fd42b23b9ef792059b1c1a89555443abbb11578f4b3c8430b452559eec7325f3\"" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { - useSupportLibrary true + useSupportLibrary = true } resourceConfigurations += [ 'ar', @@ -107,7 +107,7 @@ android { buildTypes { release { proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), 'proguard-rules.pro' - minifyEnabled true + minifyEnabled = true } debug { applicationIdSuffix '.debug' @@ -119,8 +119,8 @@ android { applicationIdSuffix '.benchmark' versionNameSuffix '-BENCHMARK' resValue "string", "app_name", "@string/app_name_benchmark" - profileable true - signingConfig signingConfigs.debug + profileable = true + signingConfig = signingConfigs.debug } } @@ -139,10 +139,10 @@ android { splits { abi { - enable true + enable = true reset() include "x86", "x86_64", "arm64-v8a", "armeabi-v7a" - universalApk true + universalApk = true } } @@ -152,8 +152,8 @@ android { } buildFeatures { - compose true - buildConfig true + compose = true + buildConfig = true } packagingOptions { diff --git a/ammolite/build.gradle b/ammolite/build.gradle index ecd515719d..ec74b88080 100644 --- a/ammolite/build.gradle +++ b/ammolite/build.gradle @@ -6,12 +6,12 @@ plugins { } android { - namespace 'com.vitorpamplona.ammolite' - compileSdk libs.versions.android.compileSdk.get().toInteger() + namespace = 'com.vitorpamplona.ammolite' + compileSdk = libs.versions.android.compileSdk.get().toInteger() defaultConfig { - minSdk libs.versions.android.minSdk.get().toInteger() - targetSdk libs.versions.android.targetSdk.get().toInteger() + minSdk = libs.versions.android.minSdk.get().toInteger() + targetSdk = libs.versions.android.targetSdk.get().toInteger() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -23,12 +23,12 @@ android { buildTypes { release { - minifyEnabled true + minifyEnabled = true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } create("benchmark") { initWith(getByName("release")) - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } compileOptions { diff --git a/benchmark/build.gradle b/benchmark/build.gradle index 2f907d1718..a45aa4aaeb 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -7,12 +7,12 @@ plugins { } android { - namespace 'com.vitorpamplona.amethyst.benchmark' - compileSdk libs.versions.android.compileSdk.get().toInteger() + namespace = 'com.vitorpamplona.amethyst.benchmark' + compileSdk = libs.versions.android.compileSdk.get().toInteger() defaultConfig { - minSdk libs.versions.android.minSdk.get().toInteger() - targetSdk libs.versions.android.targetSdk.get().toInteger() + minSdk = libs.versions.android.minSdk.get().toInteger() + targetSdk = libs.versions.android.targetSdk.get().toInteger() // Enable measuring on an emulator, or devices with low battery testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner' @@ -36,12 +36,12 @@ android { } release { isDefault = false - minifyEnabled true + minifyEnabled = true } create("benchmark") { isDefault = true initWith(getByName("release")) - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } } diff --git a/commons/build.gradle b/commons/build.gradle index 37614f930e..82c054688c 100644 --- a/commons/build.gradle +++ b/commons/build.gradle @@ -7,12 +7,12 @@ plugins { } android { - namespace 'com.vitorpamplona.amethyst.commons' - compileSdk libs.versions.android.compileSdk.get().toInteger() + namespace = 'com.vitorpamplona.amethyst.commons' + compileSdk = libs.versions.android.compileSdk.get().toInteger() defaultConfig { - minSdk libs.versions.android.minSdk.get().toInteger() - targetSdk libs.versions.android.targetSdk.get().toInteger() + minSdk = libs.versions.android.minSdk.get().toInteger() + targetSdk = libs.versions.android.targetSdk.get().toInteger() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -25,7 +25,7 @@ android { } create("benchmark") { initWith(getByName("release")) - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } compileOptions { @@ -34,7 +34,7 @@ android { } buildFeatures { - compose true + compose = true } } diff --git a/quartz/build.gradle b/quartz/build.gradle index 2b584d077b..d022c5f5bc 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -6,12 +6,12 @@ plugins { } android { - namespace 'com.vitorpamplona.quartz' - compileSdk libs.versions.android.compileSdk.get().toInteger() + namespace = 'com.vitorpamplona.quartz' + compileSdk = libs.versions.android.compileSdk.get().toInteger() defaultConfig { - minSdk libs.versions.android.minSdk.get().toInteger() - targetSdk libs.versions.android.targetSdk.get().toInteger() + minSdk = libs.versions.android.minSdk.get().toInteger() + targetSdk = libs.versions.android.targetSdk.get().toInteger() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles "consumer-rules.pro" @@ -19,12 +19,12 @@ android { buildTypes { release { - minifyEnabled true + minifyEnabled = true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } create("benchmark") { initWith(getByName("release")) - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } compileOptions { diff --git a/settings.gradle b/settings.gradle index 4208f4fde2..0a0b52faa4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,7 +10,7 @@ pluginManagement { mavenCentral() gradlePluginPortal() maven { - url "https://jitpack.io" + url = "https://jitpack.io" content { includeModule 'com.github.UnifiedPush', 'android-connector' } @@ -23,8 +23,8 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven { url "https://jitpack.io" } - maven { url "https://raw.githubusercontent.com/guardianproject/gpmaven/master" } + maven { url = "https://jitpack.io" } + maven { url = "https://raw.githubusercontent.com/guardianproject/gpmaven/master" } } } From e7f04ad38da52293f9631d5173dc5c7c8f94d0fb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 24 Jul 2025 10:01:20 -0400 Subject: [PATCH 129/185] updates spotless --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2238a677fa..599e76481e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,7 +41,7 @@ runner = "1.6.2" rfc3986 = "0.1.2" secp256k1KmpJniAndroid = "0.18.0" securityCryptoKtx = "1.1.0-beta01" -spotless = "7.1.0" +spotless = "7.2.1" torAndroid = "0.4.8.17" translate = "17.0.3" unifiedpush = "3.0.10" From b27a12b9b5e1039a5618bbd98ddb747ea48eb55b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 24 Jul 2025 10:11:36 -0400 Subject: [PATCH 130/185] Finally solved the issue with @aar in gradle --- quartz/build.gradle | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/quartz/build.gradle b/quartz/build.gradle index d022c5f5bc..d6e191a402 100644 --- a/quartz/build.gradle +++ b/quartz/build.gradle @@ -57,11 +57,12 @@ dependencies { // LibSodium for ChaCha encryption (NIP-44) // Wait for @aar support in version catalogs - implementation 'com.goterl:lazysodium-android:5.2.0@aar' - implementation 'net.java.dev.jna:jna:5.17.0@aar' - //implementation (libs.lazysodium.android) { artifact { type = "aar" } } - //implementation (libs.jna) { artifact { type = "aar" } } + // implementation 'com.goterl:lazysodium-android:5.2.0@aar' + // implementation 'net.java.dev.jna:jna:5.17.0@aar' + + implementation variantOf(libs.lazysodium.android) { artifactType("aar") } + implementation variantOf(libs.jna) { artifactType("aar") } // Performant Parser of JSONs into Events api libs.jackson.module.kotlin From 803b231aaac831ab1a16564e800c2e018ef725f8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 24 Jul 2025 10:16:56 -0400 Subject: [PATCH 131/185] Moves firebase to the main module --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 599e76481e..604fa82394 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -101,7 +101,7 @@ coil-svg = { group = "io.coil-kt.coil3", name = "coil-svg", version.ref = "coil" coil-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" } drfonfon-geohash = { group = "com.github.drfonfon", name = "android-kotlin-geohash", version.ref = "androidKotlinGeohash" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } -firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" } +firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version.ref = "languageId" } google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" } jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" } From b77127df77ac048bbdbeaef4b613b49ae5bcca40 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 24 Jul 2025 11:05:08 -0400 Subject: [PATCH 132/185] Minor adjustment --- .../amethyst/service/okhttp/OkHttpClientFactory.kt | 2 +- .../java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index a6aabc68ff..16918a4a96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -27,7 +27,7 @@ import java.net.Proxy import java.time.Duration class OkHttpClientFactory( - val keyCache: EncryptionKeyCache, + keyCache: EncryptionKeyCache, ) { companion object { // by picking a random proxy port, the connection will fail as it shouold. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt index 186cb844f5..0261ebd111 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountScreen.kt @@ -46,7 +46,7 @@ fun AccountScreen( ) { val accountState by accountStateViewModel.accountContent.collectAsStateWithLifecycle() - Log.d("ManageRelayServices", "AccountScreen $accountState $accountStateViewModel") + Log.d("ActivityLifecycle", "AccountScreen $accountState $accountStateViewModel") Crossfade( targetState = accountState, From 180a1eb1db4ea2c6588db3f8d60054b92bb64824 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Jul 2025 12:39:11 -0400 Subject: [PATCH 133/185] Refactors the use of Gzip --- .../quartz/nip19Bech32/entities/NEmbed.kt | 17 ++------- .../com/vitorpamplona/quartz/utils/GZip.kt | 37 +++++++++++++++++++ 2 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/utils/GZip.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEmbed.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEmbed.kt index b5d4a028ae..00ebc15943 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEmbed.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip19Bech32/entities/NEmbed.kt @@ -23,9 +23,7 @@ package com.vitorpamplona.quartz.nip19Bech32.entities import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip19Bech32.toNEmbed -import java.io.ByteArrayOutputStream -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream +import com.vitorpamplona.quartz.utils.GZip @Immutable data class NEmbed( @@ -34,18 +32,9 @@ data class NEmbed( companion object { fun parse(bytes: ByteArray): NEmbed? { if (bytes.isEmpty()) return null - return NEmbed(Event.fromJson(ungzip(bytes))) + return NEmbed(Event.fromJson(GZip.decompress(bytes))) } - fun create(event: Event): String = gzip(event.toJson()).toNEmbed() - - fun gzip(content: String): ByteArray { - val bos = ByteArrayOutputStream() - GZIPOutputStream(bos).bufferedWriter(Charsets.UTF_8).use { it.write(content) } - val array = bos.toByteArray() - return array - } - - fun ungzip(content: ByteArray): String = GZIPInputStream(content.inputStream()).bufferedReader(Charsets.UTF_8).use { it.readText() } + fun create(event: Event): String = GZip.compress(event.toJson()).toNEmbed() } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/GZip.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/GZip.kt new file mode 100644 index 0000000000..8bba32709d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/GZip.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.utils + +import java.io.ByteArrayOutputStream +import java.util.zip.GZIPInputStream +import java.util.zip.GZIPOutputStream + +class GZip { + companion object { + fun compress(content: String): ByteArray { + val bos = ByteArrayOutputStream() + GZIPOutputStream(bos).bufferedWriter(Charsets.UTF_8).use { it.write(content) } + return bos.toByteArray() + } + + fun decompress(content: ByteArray): String = GZIPInputStream(content.inputStream()).bufferedReader(Charsets.UTF_8).use { it.readText() } + } +} From 8844bb73ead277c1f6062b5ad403020389ea5ec1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Jul 2025 12:39:38 -0400 Subject: [PATCH 134/185] Defines a searcheable interface for events to implement and provide content for a full text search --- .../quartz/nip50Search/SearchableEvent.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchableEvent.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchableEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchableEvent.kt new file mode 100644 index 0000000000..e583e4ab0f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip50Search/SearchableEvent.kt @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip50Search + +interface SearchableEvent { + fun indexableContent(): String +} From 96e26cfb7de4fdc807fcd0bd64d7c1c16486d36b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Jul 2025 12:39:55 -0400 Subject: [PATCH 135/185] Adds a parser and assembler for NIP47 --- .../quartz/nip47WalletConnect/Nip47WalletConnect.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt index 22c696dac1..c3cf1add28 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt @@ -21,8 +21,10 @@ package com.vitorpamplona.quartz.nip47WalletConnect import androidx.core.net.toUri +import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey @@ -71,6 +73,12 @@ class Nip47WalletConnect { secret, ) } + + companion object { + fun parser(json: String) = JsonMapper.mapper.readValue(json) + + fun serializer(value: Nip47URI) = JsonMapper.mapper.writeValueAsString(value) + } } data class Nip47URINorm( From 39152d4e909fd0d868712bb0694ffe8db884ba2f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Jul 2025 12:40:55 -0400 Subject: [PATCH 136/185] Makes the metadata event and contact list addressables --- .../vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt | 4 ++-- .../vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt index 67e8266a40..7a0ab6f327 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt @@ -24,7 +24,7 @@ import android.util.Log import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.builder @@ -58,7 +58,7 @@ class MetadataEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { fun contactMetadataJson() = jacksonObjectMapper().readTree(content) as? ObjectNode fun contactMetaData() = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt index 4ad0717ac7..77bbc010e9 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.quartz.nip02FollowList import androidx.compose.runtime.Stable -import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider @@ -44,7 +44,7 @@ class ContactListEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig), +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), AddressHintProvider, PubKeyHintProvider { override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) From e7a9890eb17bc120f61316fc68aee334684cb9d5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Jul 2025 12:41:43 -0400 Subject: [PATCH 137/185] Fixes quartz test cases for the new structure --- .../quartz/nip10Notes/ThreadingTests.kt | 7 +- .../quartz/nip19Bech32/NIP19EmbedTests.kt | 76 +++++++------------ .../quartz/nip46RemoteSigner/Nip46Test.kt | 19 +++-- .../quartz/nip57Zaps/PrivateZapTests.kt | 10 ++- .../nip46RemoteSigner/BunkerResponseEvent.kt | 5 +- 5 files changed, 56 insertions(+), 61 deletions(-) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/ThreadingTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/ThreadingTests.kt index b5e4efefa9..3e10f561dc 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/ThreadingTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip10Notes/ThreadingTests.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag @@ -186,7 +187,7 @@ class ThreadingTests { val taggedUsers = listOf( - PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", "wss://goiaba.com"), + PTag("4ca4f5533e40da5e0508796d409e6bb35a50b26fc304345617ab017183d83ac0", RelayUrlNormalizer.normalize("wss://goiaba.com")), PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec"), PTag("77ce56f89d1228f7ff3743ce1ad1b254857b9008564727ebd5a1f317362f6ca7"), ) @@ -249,7 +250,7 @@ class ThreadingTests { val taggedUsers = listOf( - PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"), + PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", RelayUrlNormalizer.normalize("wss://banana.com")), ) assertEquals(taggedUsers, note.taggedUsers()) @@ -302,7 +303,7 @@ class ThreadingTests { val taggedUsers = listOf( - PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", "wss://banana.com"), + PTag("534780e44da7b494485e85cd4cca6af4f6caa1627472432b6f2a4ece0e9e54ec", RelayUrlNormalizer.normalize("wss://banana.com")), ) assertEquals(taggedUsers, note.taggedUsers()) diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt index cca3b5c4a9..347ecf81f9 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip19Bech32/NIP19EmbedTests.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.nip19Bech32 import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent -import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal @@ -33,6 +32,7 @@ import com.vitorpamplona.quartz.utils.Hex import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNotNull import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith @@ -48,18 +48,12 @@ class NIP19EmbedTests { KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), ) - var textNote: Event? = null - - val countDownLatch = CountDownLatch(1) - - signer.sign( - TextNoteEvent.build("I like this. It could solve the ninvite problem in #1062, and it seems like it could be applied very broadly to limit the spread of events that shouldn't stand on their own or need to be private. The one question I have is how long are these embeds? If it's 50 lines of text, that breaks the human readable (or at least parseable) requirement of kind 1s. Also, encoding json in a tlv is silly, we should at least use the tlv to reduce the payload size."), - ) { - textNote = it - countDownLatch.countDown() - } - - Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + val textNote: TextNoteEvent = + runBlocking { + signer.sign( + TextNoteEvent.build("I like this. It could solve the ninvite problem in #1062, and it seems like it could be applied very broadly to limit the spread of events that shouldn't stand on their own or need to be private. The one question I have is how long are these embeds? If it's 50 lines of text, that breaks the human readable (or at least parseable) requirement of kind 1s. Also, encoding json in a tlv is silly, we should at least use the tlv to reduce the payload size."), + ) + } assertNotNull(textNote) @@ -81,29 +75,23 @@ class NIP19EmbedTests { KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), ) - var eyeglassesPrescriptionEvent: Event? = null - - val countDownLatch = CountDownLatch(1) - - signer.sign(FhirResourceEvent.build(visionPrescriptionFhir)) { - eyeglassesPrescriptionEvent = it - countDownLatch.countDown() - } - - Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + val eyeglassesPrescriptionEvent = + runBlocking { + signer.sign(FhirResourceEvent.build(visionPrescriptionFhir)) + } assertNotNull(eyeglassesPrescriptionEvent) - val bech32 = NEmbed.create(eyeglassesPrescriptionEvent!!) + val bech32 = NEmbed.create(eyeglassesPrescriptionEvent) - println(eyeglassesPrescriptionEvent!!.toJson()) + println(eyeglassesPrescriptionEvent.toJson()) println(bech32) val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event assertTrue(decodedNote.verify()) - assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) + assertEquals(eyeglassesPrescriptionEvent.toJson(), decodedNote.toJson()) } @Test @@ -113,29 +101,27 @@ class NIP19EmbedTests { KeyPair(Hex.decode("e8e7197ccc53c9ed4cf9b1c8dce085475fa1ffdd71f2c14e44fe23d0bdf77598")), ) - var eyeglassesPrescriptionEvent: Event? = null - val countDownLatch = CountDownLatch(1) - signer.sign(FhirResourceEvent.build(visionPrescriptionBundle)) { - eyeglassesPrescriptionEvent = it - countDownLatch.countDown() - } + val eyeglassesPrescriptionEvent = + runBlocking { + signer.sign(FhirResourceEvent.build(visionPrescriptionBundle)) + } Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) assertNotNull(eyeglassesPrescriptionEvent) - val bech32 = NEmbed.create(eyeglassesPrescriptionEvent!!) + val bech32 = NEmbed.create(eyeglassesPrescriptionEvent) - println(eyeglassesPrescriptionEvent!!.toJson()) + println(eyeglassesPrescriptionEvent.toJson()) println(bech32) val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event assertTrue(decodedNote.verify()) - assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) + assertEquals(eyeglassesPrescriptionEvent.toJson(), decodedNote.toJson()) } @Test @@ -145,29 +131,23 @@ class NIP19EmbedTests { KeyPair(decodePrivateKeyAsHexOrNull("nsec1arn3jlxv20y76n8ek8ydecy9ga06rl7aw8evznjylc3ap00hwkvqx4vvy6")!!.hexToByteArray()), ) - var eyeglassesPrescriptionEvent: Event? = null - - val countDownLatch = CountDownLatch(1) - - signer.sign(FhirResourceEvent.build(visionPrescriptionBundle2)) { - eyeglassesPrescriptionEvent = it - countDownLatch.countDown() - } - - Assert.assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + val eyeglassesPrescriptionEvent = + runBlocking { + signer.sign(FhirResourceEvent.build(visionPrescriptionBundle2)) + } assertNotNull(eyeglassesPrescriptionEvent) - val bech32 = NEmbed.create(eyeglassesPrescriptionEvent!!) + val bech32 = NEmbed.create(eyeglassesPrescriptionEvent) - println(eyeglassesPrescriptionEvent!!.toJson()) + println(eyeglassesPrescriptionEvent.toJson()) println(bech32) val decodedNote = (Nip19Parser.uriToRoute(bech32)?.entity as NEmbed).event assertTrue(decodedNote.verify()) - assertEquals(eyeglassesPrescriptionEvent!!.toJson(), decodedNote.toJson()) + assertEquals(eyeglassesPrescriptionEvent.toJson(), decodedNote.toJson()) } @Test diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt index 9de3c4188f..61794b0964 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip46RemoteSigner/Nip46Test.kt @@ -23,8 +23,10 @@ package com.vitorpamplona.quartz.nip46RemoteSigner import androidx.test.ext.junit.runners.AndroidJUnit4 import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip02FollowList.ReadWrite +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.utils.TimeUtils import junit.framework.TestCase.assertEquals import kotlinx.coroutines.runBlocking @@ -39,11 +41,18 @@ internal class Nip46Test { val peer = NostrSignerInternal(KeyPair()) val dummyEvent = - Event( + EventTemplate( + createdAt = TimeUtils.now(), + kind = 1, + tags = emptyArray(), + content = "test", + ) + + val dummyEventSigned = + TextNoteEvent( id = "", pubKey = "", createdAt = TimeUtils.now(), - kind = 1, tags = emptyArray(), content = "test", sig = "", @@ -63,7 +72,7 @@ internal class Nip46Test { assertEquals(BunkerRequestSign.METHOD_NAME, actual.method) assertEquals(expected.id, actual.id) - assertEquals(dummyEvent.id, actual.event.id) + assertEquals(dummyEvent.createdAt, actual.event.createdAt) } @Test @@ -192,13 +201,13 @@ internal class Nip46Test { @Test fun testEventResponse() = runBlocking { - val expected = BunkerResponseEvent(event = dummyEvent) + val expected = BunkerResponseEvent(event = dummyEventSigned) val actual = encodeDecodeEvent(expected) assertEquals(expected.id, actual.id) assertEquals(expected.result, actual.result) assertEquals(expected.error, actual.error) - assertEquals(dummyEvent.id, actual.event.id) + assertEquals(dummyEventSigned.id, actual.event.id) } @Test diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt index 70403da0ec..35f720ff30 100644 --- a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapTests.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip57Zaps.PrivateZapEncryption.Companion.createEncryptionPrivateKey import com.vitorpamplona.quartz.utils.Hex @@ -93,7 +94,7 @@ class PrivateZapTests { runBlocking { LnZapRequestEvent.create( zappedEvent = poll, - relays = setOf("wss://relay.damus.io/"), + relays = setOf(RelayUrlNormalizer.normalize("wss://relay.damus.io/")), signer = loggedIn, pollOption = 0, message = "", @@ -153,7 +154,12 @@ class PrivateZapTests { runBlocking { LnZapRequestEvent.create( zappedEvent = textNote, - relays = setOf("wss://relay.damus.io/", "wss://relay.damus2.io/", "wss://relay.damus3.io/"), + relays = + setOf( + RelayUrlNormalizer.normalize("wss://relay.damus.io/"), + RelayUrlNormalizer.normalize("wss://relay.damus2.io/"), + RelayUrlNormalizer.normalize("wss://relay.damus3.io/"), + ), signer = loggedIn, pollOption = null, message = "test", diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseEvent.kt index d8e0d7e433..a7df25d165 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponseEvent.kt @@ -26,13 +26,12 @@ import java.util.UUID class BunkerResponseEvent( id: String = UUID.randomUUID().toString(), val event: Event, -) : com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse(id, event.toJson(), null) { +) : BunkerResponse(id, event.toJson(), null) { companion object { fun parse( id: String, result: String, error: String? = null, - ) = com.vitorpamplona.quartz.nip46RemoteSigner - .BunkerResponseEvent(id, Event.fromJson(result)) + ) = BunkerResponseEvent(id, Event.fromJson(result)) } } From ca9777be8306f37ac92956cc1b1ed27527c86906 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Jul 2025 12:42:19 -0400 Subject: [PATCH 138/185] Creates a kind type alias --- .../quartz/nip01Core/core/Event.kt | 2 +- .../quartz/nip01Core/core/Kind.kt | 32 +++++++++++++++++++ .../nip01Core/tags/addressables/Address.kt | 3 +- 3 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Kind.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt index 806b499961..406969a17c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt @@ -34,7 +34,7 @@ open class Event( val id: HexKey, val pubKey: HexKey, val createdAt: Long, - val kind: Int, + val kind: Kind, val tags: TagArray, val content: String, val sig: HexKey, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Kind.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Kind.kt new file mode 100644 index 0000000000..0aea6ba6f0 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Kind.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.core + +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent + +typealias Kind = Int + +fun Kind.isEphemeral() = this >= 20_000 && this < 30_000 + +fun Kind.isReplaceable() = this == MetadataEvent.KIND || this == ContactListEvent.KIND || (this >= 10_000 && this < 20_000) + +fun Kind.isAddressable() = this >= 30_000 && this < 40_000 diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt index 3ce8551533..c2ad939015 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/Address.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.tags.addressables import android.util.Log import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.utils.Hex @@ -29,7 +30,7 @@ import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes data class Address( - val kind: Int, + val kind: Kind, val pubKeyHex: HexKey, val dTag: String = "", ) : Comparable
{ From 3a37d5e42d908d5586ccb633da1bcdc1e9bf0ca7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Jul 2025 14:29:14 -0400 Subject: [PATCH 139/185] Fixes the benchmark testing cases --- .../benchmark/BaseLargeCacheBenchmark.kt | 63 +++++ .../quartz/benchmark/EventBenchmark.kt | 5 +- .../benchmark/GiftWrapReceivingBenchmark.kt | 102 ++++----- .../benchmark/GiftWrapSigningBenchmark.kt | 216 ++++++++---------- .../quartz/benchmark/HintIndexerBenchmark.kt | 2 + .../quartz/benchmark/LargeCacheBenchmark.kt | 44 +--- 6 files changed, 207 insertions(+), 225 deletions(-) create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt new file mode 100644 index 0000000000..bcf917ec01 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.benchmark + +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.utils.LargeCache +import org.junit.Assert.assertTrue +import java.util.function.Consumer +import java.util.zip.GZIPInputStream + +open class BaseLargeCacheBenchmark { + fun getEventDB(): List { + // This file includes duplicates + val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") + + return JsonMapper.mapper.readValue>( + GZIPInputStream(fullDBInputStream), + ) + } + + fun getLargeCache(db: List): LargeCache { + val cache = LargeCache() + + db.forEach { + cache.getOrCreate(it.id) { key -> + it + } + } + + return cache + } + + fun hasId(event: Event) { + assertTrue(event.id.isNotEmpty()) + } + + val consumer = + Consumer { + hasId(it) + } +} diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt index 0820f45607..d7489336bc 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventBenchmark.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.verify import com.vitorpamplona.quartz.nip01Core.verifyId import com.vitorpamplona.quartz.nip01Core.verifySignature +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.utils.TimeUtils import junit.framework.TestCase.assertTrue import org.junit.Rule @@ -90,7 +91,7 @@ class EventBenchmark { val now = TimeUtils.now() val tags = arrayOf(arrayOf("")) benchmarkRule.measureRepeated { - EventFactory.create("id", "pubkey", now, 1, tags, "content", "sig") + EventFactory.create("id", "pubkey", now, 1, tags, "content", "sig") } } @@ -99,7 +100,7 @@ class EventBenchmark { val now = TimeUtils.now() val tags = arrayOf(arrayOf("")) benchmarkRule.measureRepeated { - EventFactory.create("id", "pubkey", now, 30818, tags, "content", "sig") + EventFactory.create("id", "pubkey", now, 30818, tags, "content", "sig") } } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt index 897b462244..3472b7794e 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapReceivingBenchmark.kt @@ -40,12 +40,10 @@ import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import junit.framework.TestCase.assertNotNull -import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit /** * Benchmark, which will execute on an Android device. @@ -60,78 +58,56 @@ class GiftWrapReceivingBenchmark { fun createWrap( sender: NostrSigner, receiver: NostrSigner, - ): GiftWrapEvent { - val countDownLatch = CountDownLatch(1) - var wrap: GiftWrapEvent? = null - - sender.sign( - ChatMessageEvent.build( - msg = "Hi there! This is a test message", - to = - listOf( - PTag(receiver.pubKey), + ): GiftWrapEvent = + runBlocking { + GiftWrapEvent.create( + event = + SealedRumorEvent.create( + event = + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = listOf(PTag(receiver.pubKey)), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, + ), + encryptTo = receiver.pubKey, + signer = sender, ), - ) { - changeSubject("Party Tonight") - zapraiser(10000) - contentWarning("nsfw") - }, - ) { - SealedRumorEvent.create( - event = it, - encryptTo = receiver.pubKey, - signer = sender, - ) { - GiftWrapEvent.create( - event = it, - recipientPubKey = receiver.pubKey, - ) { - wrap = it - countDownLatch.countDown() - } - } + recipientPubKey = receiver.pubKey, + ) } - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - - return wrap!! - } - fun createSeal( sender: NostrSigner, receiver: NostrSigner, - ): SealedRumorEvent { - val countDownLatch = CountDownLatch(1) - var seal: SealedRumorEvent? = null + ): SealedRumorEvent = + runBlocking { + val msg = + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, + ) - sender.sign( - ChatMessageEvent.build( - msg = "Hi there! This is a test message", - to = - listOf( - PTag(receiver.pubKey), - ), - ) { - changeSubject("Party Tonight") - zapraiser(10000) - contentWarning("nsfw") - }, - ) { SealedRumorEvent.create( - event = it, + event = msg, encryptTo = receiver.pubKey, signer = sender, - ) { - seal = it - countDownLatch.countDown() - } + ) } - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - - return seal!! - } - @Test fun parseWrapFromString() { val sender = NostrSignerInternal(KeyPair()) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt index 9e1a770c11..3c310d28dd 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/GiftWrapSigningBenchmark.kt @@ -32,12 +32,10 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit /** * Benchmark, which will execute on an Android device. @@ -55,25 +53,21 @@ class GiftWrapSigningBenchmark { val receiver = NostrSignerInternal(KeyPair()) benchmarkRule.measureRepeated { - val countDownLatch = CountDownLatch(1) - - sender.sign( - ChatMessageEvent.build( - msg = "Hi there! This is a test message", - to = - listOf( - PTag(receiver.pubKey), - ), - ) { - changeSubject("Party Tonight") - zapraiser(10000) - contentWarning("nsfw") - }, - ) { - countDownLatch.countDown() + runBlocking { + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, + ) } - - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) } } @@ -82,40 +76,31 @@ class GiftWrapSigningBenchmark { val sender = NostrSignerInternal(KeyPair()) val receiver = NostrSignerInternal(KeyPair()) - val countDownLatch = CountDownLatch(1) - - var msg: ChatMessageEvent? = null - - sender.sign( - ChatMessageEvent.build( - msg = "Hi there! This is a test message", - to = - listOf( - PTag(receiver.pubKey), - ), - ) { - changeSubject("Party Tonight") - zapraiser(10000) - contentWarning("nsfw") - }, - ) { - msg = it - countDownLatch.countDown() - } - - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) - - benchmarkRule.measureRepeated { - val countDownLatch2 = CountDownLatch(1) - SealedRumorEvent.create( - event = msg!!, - encryptTo = receiver.pubKey, - signer = sender, - ) { - countDownLatch2.countDown() + val msg = + runBlocking { + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, + ) } - assertTrue(countDownLatch2.await(1, TimeUnit.SECONDS)) + benchmarkRule.measureRepeated { + runBlocking { + SealedRumorEvent.create( + event = msg, + encryptTo = receiver.pubKey, + signer = sender, + ) + } } } @@ -124,44 +109,39 @@ class GiftWrapSigningBenchmark { val sender = NostrSignerInternal(KeyPair()) val receiver = NostrSignerInternal(KeyPair()) - val countDownLatch = CountDownLatch(1) - - var seal: SealedRumorEvent? = null - - sender.sign( - ChatMessageEvent.build( - msg = "Hi there! This is a test message", - to = - listOf( - PTag(receiver.pubKey), - ), - ) { - changeSubject("Party Tonight") - zapraiser(10000) - contentWarning("nsfw") - }, - ) { - SealedRumorEvent.create( - event = it, - encryptTo = receiver.pubKey, - signer = sender, - ) { - seal = it - countDownLatch.countDown() + val msg = + runBlocking { + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, + ) } - } - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + val seal = + runBlocking { + SealedRumorEvent.create( + event = msg, + encryptTo = receiver.pubKey, + signer = sender, + ) + } benchmarkRule.measureRepeated { - val countDownLatch2 = CountDownLatch(1) - GiftWrapEvent.create( - event = seal!!, - recipientPubKey = receiver.pubKey, - ) { - countDownLatch2.countDown() + runBlocking { + GiftWrapEvent.create( + event = seal!!, + recipientPubKey = receiver.pubKey, + ) } - assertTrue(countDownLatch2.await(1, TimeUnit.SECONDS)) } } @@ -170,40 +150,40 @@ class GiftWrapSigningBenchmark { val sender = NostrSignerInternal(KeyPair()) val receiver = NostrSignerInternal(KeyPair()) - val countDownLatch = CountDownLatch(1) - - var wrap: GiftWrapEvent? = null - - sender.sign( - ChatMessageEvent.build( - msg = "Hi there! This is a test message", - to = - listOf( - PTag(receiver.pubKey), - ), - ) { - changeSubject("Party Tonight") - zapraiser(10000) - contentWarning("nsfw") - }, - ) { - SealedRumorEvent.create( - event = it, - encryptTo = receiver.pubKey, - signer = sender, - ) { - GiftWrapEvent.create( - event = it, - recipientPubKey = receiver.pubKey, - ) { - wrap = it - countDownLatch.countDown() - } + val msg = + runBlocking { + sender.sign( + ChatMessageEvent.build( + msg = "Hi there! This is a test message", + to = + listOf( + PTag(receiver.pubKey), + ), + ) { + changeSubject("Party Tonight") + zapraiser(10000) + contentWarning("nsfw") + }, + ) } - } - assertTrue(countDownLatch.await(1, TimeUnit.SECONDS)) + val seal = + runBlocking { + SealedRumorEvent.create( + event = msg, + encryptTo = receiver.pubKey, + signer = sender, + ) + } - benchmarkRule.measureRepeated { wrap!!.toJson() } + val wrap = + runBlocking { + GiftWrapEvent.create( + event = seal, + recipientPubKey = receiver.pubKey, + ) + } + + benchmarkRule.measureRepeated { wrap.toJson() } } } diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt index 0b8150e217..80c16fdfe9 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HintIndexerBenchmark.kt @@ -27,6 +27,7 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.hints.HintIndexer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.RandomInstance import org.junit.Rule import org.junit.Test @@ -52,6 +53,7 @@ class HintIndexerBenchmark { .readBytes() .toString(Charset.forName("utf-8")) .split("\n") + .mapNotNull(RelayUrlNormalizer::normalizeOrNull) } @Test diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt index a64b13d845..325c72329c 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeCacheBenchmark.kt @@ -23,54 +23,14 @@ package com.vitorpamplona.quartz.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import com.fasterxml.jackson.module.kotlin.readValue -import com.vitorpamplona.quartz.nip01Core.core.Event -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper -import com.vitorpamplona.quartz.utils.LargeCache -import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import java.util.Arrays -import java.util.function.Consumer -import java.util.zip.GZIPInputStream - -open class BaseLargeCacheBenchmark { - fun getEventDB(): List { - // This file includes duplicates - val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") - - return JsonMapper.mapper.readValue>( - GZIPInputStream(fullDBInputStream), - ) - } - - fun getLargeCache(db: List): LargeCache { - val cache = LargeCache() - - db.forEach { - cache.getOrCreate(it.id) { key -> - it - } - } - - return cache - } - - fun hasId(event: Event) { - assertTrue(event.id.isNotEmpty()) - } - - val consumer = - Consumer { - hasId(it) - } -} +import kotlin.collections.distinctBy @RunWith(AndroidJUnit4::class) -class LargeCacheForEachBenchmark : BaseLargeCacheBenchmark() { +class LargeCacheBenchmark : BaseLargeCacheBenchmark() { @get:Rule val benchmarkRule = BenchmarkRule() // 191,353 ns 0 allocs Trace EMULATOR_LargeCacheForEachBenchmark.benchForEachConsumerList From f8f59abdf11e2d6a6aca1e1649c42675f4745866 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 25 Jul 2025 14:33:14 -0400 Subject: [PATCH 140/185] Small refactoring --- .../amethyst/ui/screen/AccountStateViewModel.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index 12b91d0680..d471791146 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -94,10 +94,18 @@ class AccountStateViewModel : ViewModel() { } } - private suspend fun tryLoginExistingAccount(route: Route? = null) = - withContext(Dispatchers.IO) { - LocalPreferences.loadCurrentAccountFromEncryptedStorage() - }?.let { startUI(it, route) } ?: run { requestLoginUI() } + private suspend fun tryLoginExistingAccount(route: Route? = null) { + val accountSettings = + withContext(Dispatchers.IO) { + LocalPreferences.loadCurrentAccountFromEncryptedStorage() + } + + if (accountSettings != null) { + startUI(accountSettings, route) + } else { + requestLoginUI() + } + } private suspend fun requestLoginUI() { _accountContent.update { AccountState.LoggedOff } From dc68d6025eba3e0e33c089169d1380c409ee336a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Jul 2025 16:24:10 -0400 Subject: [PATCH 141/185] Adds support for the Request to Vanish NIP - 62 --- .../com/vitorpamplona/quartz/EventFactory.kt | 2 + .../RequestToVanishEvent.kt | 82 +++++++++++++++++++ .../nip62RequestToVanish/tags/RelayTag.kt | 57 +++++++++++++ .../tags/TagArrayBuilderExt.kt | 32 ++++++++ .../nip62RequestToVanish/tags/TagArrayExt.kt | 28 +++++++ 5 files changed, 201 insertions(+) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayExt.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 82e80f2184..2a0fa369e6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -103,6 +103,7 @@ import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent @@ -270,6 +271,7 @@ class EventFactory { RelaySetEvent.KIND -> RelaySetEvent(id, pubKey, createdAt, tags, content, sig) ReportEvent.KIND -> ReportEvent(id, pubKey, createdAt, tags, content, sig) RepostEvent.KIND -> RepostEvent(id, pubKey, createdAt, tags, content, sig) + RequestToVanishEvent.KIND -> RequestToVanishEvent(id, pubKey, createdAt, tags, content, sig) SealedRumorEvent.KIND -> SealedRumorEvent(id, pubKey, createdAt, tags, content, sig) SearchRelayListEvent.KIND -> SearchRelayListEvent(id, pubKey, createdAt, tags, content, sig) StatusEvent.KIND -> StatusEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt new file mode 100644 index 0000000000..110ce87145 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/RequestToVanishEvent.kt @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip62RequestToVanish + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip62RequestToVanish.tags.shouldVanishFrom +import com.vitorpamplona.quartz.nip62RequestToVanish.tags.vanishFrom +import com.vitorpamplona.quartz.nip62RequestToVanish.tags.vanishFromEverywhere +import com.vitorpamplona.quartz.nip62RequestToVanish.tags.vanishFromRelays +import com.vitorpamplona.quartz.utils.TimeUtils + +class RequestToVanishEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun vanishFromRelays() = tags.vanishFromRelays() + + fun shouldVanishFrom(relayUrl: String?) = tags.shouldVanishFrom(relayUrl) + + companion object { + const val KIND = 62 + const val ALT = "Request to vanish" + + fun build( + relay: String, + reason: String = "", + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, reason, createdAt) { + alt(ALT) + vanishFrom(relay) + initializer() + } + + fun build( + relays: List, + reason: String = "", + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, reason, createdAt) { + alt(ALT) + vanishFrom(relays) + initializer() + } + + fun buildVanishFromEverywhere( + reason: String = "", + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, reason, createdAt) { + alt(ALT) + vanishFromEverywhere() + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt new file mode 100644 index 0000000000..13a1c1eb3c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/RelayTag.kt @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip62RequestToVanish.tags + +import android.R.attr.tag +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +class RelayTag { + companion object { + const val TAG_NAME = "relay" + const val EVERYWHERE = "ALL_RELAYS" + + @JvmStatic + fun match(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun notMatch(tag: Array) = tag.has(0) && tag[0] == TAG_NAME + + @JvmStatic + fun shouldVanishFrom( + tag: Array, + relayUrl: String?, + ) = tag.has(1) && tag[0] == TAG_NAME && (tag[1] == relayUrl || tag[1] == EVERYWHERE) + + @JvmStatic + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + @JvmStatic + fun assemble(relay: String) = arrayOf(TAG_NAME, relay) + + fun assembleEverywhere() = arrayOf(TAG_NAME, EVERYWHERE) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..7d29a1bf8c --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayBuilderExt.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip62RequestToVanish.tags + +import com.vitorpamplona.quartz.experimental.ephemChat.chat.relay +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent +import kotlin.collections.map + +fun TagArrayBuilder.vanishFromEverywhere() = add(RelayTag.assembleEverywhere()) + +fun TagArrayBuilder.vanishFrom(relay: String) = add(RelayTag.assemble(relay)) + +fun TagArrayBuilder.vanishFrom(relays: List) = addAll(relays.map { RelayTag.assemble(it) }) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayExt.kt new file mode 100644 index 0000000000..1a81d454a7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip62RequestToVanish/tags/TagArrayExt.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip62RequestToVanish.tags + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.core.any + +fun TagArray.vanishFromRelays() = mapNotNull(RelayTag::parse) + +fun TagArray.shouldVanishFrom(relayUrl: String?) = any(RelayTag::shouldVanishFrom, relayUrl) From d5bbf4b00b9677539f4cae5a044848cada262760 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Jul 2025 16:25:09 -0400 Subject: [PATCH 142/185] Adds basic events to the seacheable interface --- .../quartz/experimental/zapPolls/PollNoteEvent.kt | 6 +++++- .../com/vitorpamplona/quartz/nip01Core/core/Event.kt | 2 ++ .../com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt | 9 +++++++-- .../vitorpamplona/quartz/nip22Comments/CommentEvent.kt | 6 +++++- .../quartz/nip23LongContent/LongTextNoteEvent.kt | 6 +++++- .../nip28PublicChat/message/ChannelMessageEvent.kt | 6 +++++- .../vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt | 6 +++++- .../chat/LiveActivitiesChatMessageEvent.kt | 6 +++++- .../com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt | 6 +++++- .../quartz/nip84Highlights/HighlightEvent.kt | 6 +++++- 10 files changed, 49 insertions(+), 10 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt index 39c04dc958..3c9d165854 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/zapPolls/PollNoteEvent.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.nip19Bech32.eventIds import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -60,7 +61,10 @@ class PollNoteEvent( ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider, AddressHintProvider, - PubKeyHintProvider { + PubKeyHintProvider, + SearchableEvent { + override fun indexableContent() = content + pollOptionsArray().map { "\nOption: " + it.descriptor } + override fun eventHints(): List { val eHints = tags.mapNotNull(MarkedETag::parseAsHint) val qHints = tags.mapNotNull(QTag::parseEventAsHint) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt index 406969a17c..09ea417cfe 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt @@ -45,6 +45,8 @@ open class Event( */ open fun isContentEncoded() = false + open fun extraIndexableTagNames() = emptySet() + open fun countMemory(): Long = 7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) 12L + // createdAt + kind diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt index b102d696f3..44d6944669 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -36,6 +36,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip10Notes.tags.markedETags import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo +import com.vitorpamplona.quartz.nip14Subject.subject import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip19Bech32.addressHints import com.vitorpamplona.quartz.nip19Bech32.addressIds @@ -44,6 +45,7 @@ import com.vitorpamplona.quartz.nip19Bech32.eventIds import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -57,7 +59,10 @@ class TextNoteEvent( ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), EventHintProvider, AddressHintProvider, - PubKeyHintProvider { + PubKeyHintProvider, + SearchableEvent { + override fun indexableContent() = "Subject: " + subject() + "\n" + content + override fun eventHints(): List { val eHints = tags.mapNotNull(MarkedETag::parseAsHint) val qHints = tags.mapNotNull(QTag::parseEventAsHint) @@ -108,7 +113,7 @@ class TextNoteEvent( const val KIND = 1 const val ALT = "A short note: " - fun shortedMessageForAlt(msg: String): String { + private fun shortedMessageForAlt(msg: String): String { if (msg.length < 50) return ALT + msg return ALT + msg.take(50) + "..." } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt index 929a43cb8c..b68eb4a34f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip22Comments/CommentEvent.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.quartz.nip22Comments.tags.RootEventTag import com.vitorpamplona.quartz.nip22Comments.tags.RootIdentifierTag import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import com.vitorpamplona.quartz.utils.TimeUtils @@ -71,7 +72,10 @@ class CommentEvent( RootScope, EventHintProvider, PubKeyHintProvider, - AddressHintProvider { + AddressHintProvider, + SearchableEvent { + override fun indexableContent() = content + override fun pubKeyHints(): List { val pHints = tags.mapNotNull(RootAuthorTag::parseAsHint) + diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt index 5d064fcbd2..23b131b0bc 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip23LongContent/LongTextNoteEvent.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils import java.util.UUID @@ -67,7 +68,10 @@ class LongTextNoteEvent( EventHintProvider, PubKeyHintProvider, AddressHintProvider, - RootScope { + RootScope, + SearchableEvent { + override fun indexableContent() = "title: " + title() + "\nsummary: " + summary() + "\n" + content + override fun eventHints(): List { val qHints = tags.mapNotNull(QTag::parseEventAsHint) val nip19Hints = citedNIP19().eventHints() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt index 2c49c8c57a..97a0440ba6 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip28PublicChat/message/ChannelMessageEvent.kt @@ -50,6 +50,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel import com.vitorpamplona.quartz.nip28PublicChat.base.channel import com.vitorpamplona.quartz.nip28PublicChat.base.reply import com.vitorpamplona.quartz.nip37Drafts.ExposeInDraft +import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -65,7 +66,10 @@ class ChannelMessageEvent( ExposeInDraft, EventHintProvider, AddressHintProvider, - PubKeyHintProvider { + PubKeyHintProvider, + SearchableEvent { + override fun indexableContent() = content + override fun eventHints(): List { val eHints = tags.mapNotNull(MarkedETag::parseAsHint) val qHints = tags.mapNotNull(QTag::parseEventAsHint) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt index ac0f9b9b1e..3e99eea631 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip34Git/issue/GitIssueEvent.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent +import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -58,7 +59,10 @@ class GitIssueEvent( ) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), PubKeyHintProvider, EventHintProvider, - AddressHintProvider { + AddressHintProvider, + SearchableEvent { + override fun indexableContent() = "Subject: " + subject() + "\n" + content + override fun eventHints(): List { val qHints = tags.mapNotNull(QTag::parseEventAsHint) val nip19Hints = citedNIP19().eventHints() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt index 11ef4ec951..1ecedc91ef 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip53LiveActivities/chat/LiveActivitiesChatMessageEvent.kt @@ -46,6 +46,7 @@ import com.vitorpamplona.quartz.nip19Bech32.eventIds import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip37Drafts.ExposeInDraft +import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -61,7 +62,10 @@ class LiveActivitiesChatMessageEvent( EventHintProvider, PubKeyHintProvider, AddressHintProvider, - ExposeInDraft { + ExposeInDraft, + SearchableEvent { + override fun indexableContent() = content + override fun eventHints(): List { val eHints = tags.mapNotNull(ETag::parseAsHint) val qHints = tags.mapNotNull(QTag::parseEventAsHint) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index f8d65cda9a..d0996a1fce 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -46,6 +46,7 @@ import com.vitorpamplona.quartz.nip19Bech32.eventIds import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -60,7 +61,10 @@ class WikiNoteEvent( AddressableEvent, EventHintProvider, AddressHintProvider, - PubKeyHintProvider { + PubKeyHintProvider, + SearchableEvent { + override fun indexableContent() = "title: " + title() + "\nsummary: " + summary() + "\n" + content + override fun eventHints(): List { val eHints = tags.mapNotNull(MarkedETag::parseAsHint) val qHints = tags.mapNotNull(QTag::parseEventAsHint) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt index 5e8254e91d..b1be670b95 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip84Highlights/HighlightEvent.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.nip84Highlights.tags.CommentTag import com.vitorpamplona.quartz.nip84Highlights.tags.ContextTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -63,7 +64,10 @@ class HighlightEvent( RootScope, EventHintProvider, AddressHintProvider, - PubKeyHintProvider { + PubKeyHintProvider, + SearchableEvent { + override fun indexableContent() = "comment: " + comment() + "\ncontext: " + context() + "\n" + content + override fun eventHints(): List { val eHints = tags.mapNotNull(ETag::parseAsHint) val qHints = tags.mapNotNull(QTag::parseEventAsHint) From 8e3594cd7508291ee0d06602d130ed1b8babeda1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Jul 2025 16:25:24 -0400 Subject: [PATCH 143/185] Adds a way to delete events by address only --- .../quartz/nip09Deletions/DeletionEvent.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt index 45df04bd00..96d655716c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip09Deletions/DeletionEvent.kt @@ -93,6 +93,25 @@ class DeletionEvent( initializer() } + fun buildAddressOnly( + deleteEvents: List, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT) + + deleteEvents.forEach { + if (it is AddressableEvent) { + aTag(it.aTag()) + } + } + + pTagIds(deleteEvents.mapTo(HashSet()) { it.pubKey }) + kinds(deleteEvents.mapTo(HashSet()) { it.kind }) + + initializer() + } + fun buildForVersionOnly( deleteEvents: List, createdAt: Long = TimeUtils.now(), From e6dfa0752641f42f7e5f8ade36862f877fbe53ed Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Jul 2025 16:51:59 -0400 Subject: [PATCH 144/185] Fixes expiration builder --- .../vitorpamplona/quartz/nip40Expiration/TagArrayBuilderExt.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayBuilderExt.kt index 2835041a82..79e46448d5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/TagArrayBuilderExt.kt @@ -23,4 +23,4 @@ package com.vitorpamplona.quartz.nip40Expiration import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -fun TagArrayBuilder.expiration(time: Long) = addUnique(ExpirationTag.assemble(time)) +fun TagArrayBuilder.expiration(time: Long) = addUnique(ExpirationTag.assemble(time)) From a59fe596576e424711fc1f457718d6f4abded613 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 27 Jul 2025 16:52:28 -0400 Subject: [PATCH 145/185] Sending eventDB companion to avoid recreating it all the time --- .../benchmark/BaseLargeCacheBenchmark.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt index bcf917ec01..3f502cb4db 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/BaseLargeCacheBenchmark.kt @@ -31,22 +31,22 @@ import java.util.function.Consumer import java.util.zip.GZIPInputStream open class BaseLargeCacheBenchmark { - fun getEventDB(): List { - // This file includes duplicates - val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") + companion object { + fun getEventDB(): List { + // This file includes duplicates + val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") - return JsonMapper.mapper.readValue>( - GZIPInputStream(fullDBInputStream), - ) + return JsonMapper.mapper.readValue>( + GZIPInputStream(fullDBInputStream), + ) + } } fun getLargeCache(db: List): LargeCache { val cache = LargeCache() - db.forEach { - cache.getOrCreate(it.id) { key -> - it - } + db.forEach { event -> + cache.getOrCreate(event.id) { key -> event } } return cache From 0116bc30e4b96acfbdee23cdf20dfdb8180b1b66 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 28 Jul 2025 10:53:55 -0400 Subject: [PATCH 146/185] Creates a TagArray deserializer that interns all strings to avoid duplicated memory --- .../quartz/nip01Core/jackson/JsonMapper.kt | 3 +- .../nip01Core/jackson/TagArrayDeserializer.kt | 32 +++++++++++++++++++ .../jackson/TagArrayManualDeserializer.kt | 32 +++++++++++++++++++ .../quartz/nip01Core/relay/filters/Filter.kt | 4 --- 4 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayDeserializer.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayManualDeserializer.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt index 3a24f550fc..976a004b15 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt @@ -70,7 +70,8 @@ class JsonMapper { .addDeserializer(Permission::class.java, PermissionDeserializer()) .addSerializer(Permission::class.java, PermissionSerializer()) .addDeserializer(IntentResult::class.java, IntentResultJsonDeserializer()) - .addSerializer(IntentResult::class.java, IntentResultJsonSerializer()), + .addSerializer(IntentResult::class.java, IntentResultJsonSerializer()) + .addDeserializer(Array>::class.java, TagArrayDeserializer()), ) fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayDeserializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayDeserializer.kt new file mode 100644 index 0000000000..fbc44abe32 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayDeserializer.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.jackson + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.deser.std.StdDeserializer + +class TagArrayDeserializer : StdDeserializer>>(Array>::class.java) { + override fun deserialize( + jp: JsonParser, + ctxt: DeserializationContext, + ): Array> = TagArrayManualDeserializer.fromJson(jp.codec.readTree(jp)) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayManualDeserializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayManualDeserializer.kt new file mode 100644 index 0000000000..26468af33f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayManualDeserializer.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.jackson + +import com.fasterxml.jackson.databind.JsonNode + +class TagArrayManualDeserializer { + companion object { + fun fromJson(jsonObject: JsonNode): Array> = + jsonObject.toTypedArray { + it.toTypedArray { s -> if (s.isNull) "" else s.asText().intern() } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt index dcd0990501..d62ddfacef 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/filters/Filter.kt @@ -80,10 +80,6 @@ class Filter( (search != null && search.isNotEmpty()) init { - if (!isFilledFilter()) { - Log.e("FilterError", "Filter is empty: ${toJson()}") - } - ids?.forEach { if (it.length != 64) Log.e("FilterError", "Invalid id length $it on ${toJson()}") } From 88f9beafd349ded60a1bc8ea497fd19cafdf08e4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 28 Jul 2025 16:31:08 -0400 Subject: [PATCH 147/185] Faster expiration calculation and tag array serialization --- .../nip01Core/jackson/EventSerializer.kt | 9 +++- .../quartz/nip01Core/jackson/JsonMapper.kt | 3 +- .../nip01Core/jackson/TagArraySerializer.kt | 43 +++++++++++++++++++ .../quartz/nip40Expiration/ExpirationTag.kt | 3 +- 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArraySerializer.kt diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventSerializer.kt index 286a40a0d5..0e665defd0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventSerializer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventSerializer.kt @@ -24,6 +24,7 @@ import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.vitorpamplona.quartz.nip01Core.core.Event +import kotlin.collections.indices class EventSerializer : StdSerializer(Event::class.java) { override fun serialize( @@ -37,7 +38,13 @@ class EventSerializer : StdSerializer(Event::class.java) { gen.writeNumberField("created_at", event.createdAt) gen.writeNumberField("kind", event.kind) gen.writeArrayFieldStart("tags") - event.tags.forEach { tag -> gen.writeArray(tag, 0, tag.size) } + for (i in event.tags.indices) { + gen.writeStartArray() + for (j in event.tags[i].indices) { + gen.writeString(event.tags[i][j]) + } + gen.writeEndArray() + } gen.writeEndArray() gen.writeStringField("content", event.content) gen.writeStringField("sig", event.sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt index 976a004b15..ea75d82bf5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/JsonMapper.kt @@ -71,7 +71,8 @@ class JsonMapper { .addSerializer(Permission::class.java, PermissionSerializer()) .addDeserializer(IntentResult::class.java, IntentResultJsonDeserializer()) .addSerializer(IntentResult::class.java, IntentResultJsonSerializer()) - .addDeserializer(Array>::class.java, TagArrayDeserializer()), + .addDeserializer(Array>::class.java, TagArrayDeserializer()) + .addSerializer(Array>::class.java, TagArraySerializer()), ) fun fromJson(json: String): Event = mapper.readValue(json, Event::class.java) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArraySerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArraySerializer.kt new file mode 100644 index 0000000000..6152a1c5e9 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/TagArraySerializer.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.jackson + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer + +class TagArraySerializer : StdSerializer>>(Array>::class.java) { + override fun serialize( + tags: Array>, + gen: JsonGenerator, + provider: SerializerProvider, + ) { + gen.writeStartArray() + for (i in tags.indices) { + gen.writeStartArray() + for (j in tags[i].indices) { + gen.writeString(tags[i][j]) + } + gen.writeEndArray() + } + gen.writeEndArray() + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/ExpirationTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/ExpirationTag.kt index 7e178f4517..92c029f11e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/ExpirationTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip40Expiration/ExpirationTag.kt @@ -33,8 +33,7 @@ class ExpirationTag { @JvmStatic fun parse(tag: Array): Long? { - ensure(tag.has(1)) { return null } - ensure(tag[0] == TAG_NAME) { return null } + ensure(tag.has(1) && tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } return tag[1].toLongOrNull() } From 7ba2810423c1d2ff45655af199852984a977b33a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 28 Jul 2025 16:49:39 -0400 Subject: [PATCH 148/185] Quick event store using SQLite directly. --- .../benchmark/LargeDBInsertBenchmark.kt | 115 +++++ .../benchmark/LargeDBQueryingBenchmark.kt | 139 ++++++ .../nip01Core/store/sqlite/AddressableTest.kt | 104 ++++ .../nip01Core/store/sqlite/AssertUtils.kt | 83 ++++ .../nip01Core/store/sqlite/BasicTest.kt | 207 ++++++++ .../nip01Core/store/sqlite/DeletionTest.kt | 173 +++++++ .../nip01Core/store/sqlite/ExpirationTest.kt | 103 ++++ .../nip01Core/store/sqlite/LargeDBTests.kt | 104 ++++ .../nip01Core/store/sqlite/ReplaceableTest.kt | 104 ++++ .../store/sqlite/RightToVanishTest.kt | 90 ++++ .../nip01Core/store/sqlite/SearchTest.kt | 108 ++++ .../quartz/nip01Core/store/README.md | 193 +++++++ .../store/sqlite/AddressableModule.kt | 72 +++ .../store/sqlite/DeletionRequestModule.kt | 98 ++++ .../nip01Core/store/sqlite/EphemeralModule.kt | 40 ++ .../store/sqlite/EventIndexesModule.kt | 469 ++++++++++++++++++ .../nip01Core/store/sqlite/EventStore.kt | 61 +++ .../store/sqlite/ExpirationModule.kt | 92 ++++ .../store/sqlite/FullTextSearchModule.kt | 92 ++++ .../store/sqlite/ReplaceableModule.kt | 70 +++ .../store/sqlite/RightToVanishModule.kt | 111 +++++ .../store/sqlite/SQLiteEventStore.kt | 134 +++++ .../nip01Core/store/sqlite/StatementCache.kt | 50 ++ .../nip01Core/store/sqlite/sql/Condition.kt | 84 ++++ .../store/sqlite/sql/SqlSelectionBuilder.kt | 106 ++++ .../store/sqlite/sql/WhereClauseBuilder.kt | 120 +++++ .../store/sqlite/EventDbQueryAssemblerTest.kt | 133 +++++ 27 files changed, 3255 insertions(+) create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBInsertBenchmark.kt create mode 100644 benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBQueryingBenchmark.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AssertUtils.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationTest.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/LargeDBTests.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishTest.kt create mode 100644 quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/README.md create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableModule.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EphemeralModule.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationModule.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/FullTextSearchModule.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableModule.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishModule.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/StatementCache.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt create mode 100644 quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventDbQueryAssemblerTest.kt diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBInsertBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBInsertBenchmark.kt new file mode 100644 index 0000000000..9dd3897437 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBInsertBenchmark.kt @@ -0,0 +1,115 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.benchmark + +import android.content.Context +import android.database.sqlite.SQLiteException +import android.util.Log +import android.util.Log.e +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore +import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip40Expiration.isExpired +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.collections.flatten + +@RunWith(AndroidJUnit4::class) +class LargeDBInsertBenchmark : BaseLargeCacheBenchmark() { + @get:Rule val benchmarkRule = BenchmarkRule() + + companion object { + val allEvents = getEventDB().distinctBy { it.id }.filter { !it.isExpired() }.sortedBy { it.createdAt } + val firstThousandEvents = allEvents.take(1000) + } + + @Test + fun benchInserting1000Events() { + val context = ApplicationProvider.getApplicationContext() + benchmarkRule.measureRepeated { + val db = + this.runWithTimingDisabled { + val db = EventStore(context, "test1.db") + db.store.clearDB() + db + } + firstThousandEvents.forEach { event -> + try { + db.insert(event) + } catch (e: SQLiteException) { + Log.w("LargeDBInsertBenchmark", "Error inserting event: ${e.message} for event: $event") + } + } + this.runWithTimingDisabled { + db.store.clearDB() + db.close() + } + } + } + + @Test + fun bench40DeletionRequestsEvents() { + val context = ApplicationProvider.getApplicationContext() + val deletions = allEvents.filterIsInstance() + val deletionIds = deletions.map { it.deleteEventIds() }.flatten().toSet() + val deletionAddresses = deletions.map { it.deleteAddressIds() }.flatten().toSet() + + val toBeDeletedEvents = + allEvents.filter { + (it.id in deletionIds || (it is AddressableEvent && it.addressTag() in deletionAddresses)) + } + + benchmarkRule.measureRepeated { + val db = + this.runWithTimingDisabled { + val db = EventStore(context, "test1.db") + db.store.clearDB() + toBeDeletedEvents.forEach { event -> + try { + db.insert(event) + } catch (e: SQLiteException) { + Log.w("LargeDBInsertBenchmark", "Error inserting event: ${e.message} for event: $event") + } + } + + db + } + + deletions.forEach { event -> + try { + db.insert(event) + } catch (e: SQLiteException) { + Log.w("LargeDBInsertBenchmark", "Error inserting event: ${e.message} for event: $event") + } + } + + runWithTimingDisabled { + db.store.clearDB() + db.close() + } + } + } +} diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBQueryingBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBQueryingBenchmark.kt new file mode 100644 index 0000000000..805cb08b5d --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/LargeDBQueryingBenchmark.kt @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.benchmark + +import android.content.Context +import android.database.sqlite.SQLiteException +import android.util.Log +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LargeDBQueryingBenchmark : BaseLargeCacheBenchmark() { + @get:Rule val benchmarkRule = BenchmarkRule() + + companion object { + val allEvents = getEventDB().distinctBy { it.id }.sortedBy { it.createdAt } + } + + lateinit var db: EventStore + + @Before + fun setup() { + println("Running Setup") + val context = ApplicationProvider.getApplicationContext() + + db = EventStore(context, "allEvents.db") + db.store.clearDB() + allEvents.forEach { event -> + try { + db.insert(event) + } catch (e: SQLiteException) { + Log.w("LargeDBQueryingBenchmark", "Error inserting event: ${e.message} for event: $event") + } + } + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun benchQuerying1000Events() { + benchmarkRule.measureRepeated { + db.query(Filter(limit = 1000)) + } + } + + @Test + fun benchQuerying1614GiftWrapsEvents() { + benchmarkRule.measureRepeated { + db.query( + Filter( + kinds = listOf(GiftWrapEvent.KIND), + tags = mapOf("p" to listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")), + ), + ) + } + } + + @Test + fun benchQueryingEventsByAuthor() { + benchmarkRule.measureRepeated { + db.query( + Filter( + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + ) + } + } + + @Test + fun benchQueryingMetadataByAuthor() { + benchmarkRule.measureRepeated { + db.query( + Filter( + kinds = listOf(MetadataEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + ) + } + } + + @Test + fun benchQueryingRelayListByAuthor() { + benchmarkRule.measureRepeated { + db.query( + Filter( + kinds = listOf(AdvertisedRelayListEvent.KIND), + authors = listOf("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + ), + ) + } + } + + @Test + fun benchQueryingEmptyReturnByTags() { + benchmarkRule.measureRepeated { + db.query(Filter(tags = mapOf("p" to listOf("176d972f60dcdc3212ed8c92ef85065c176d972f60dcdc3212ed8c92ef85065c")))) + } + } + + @Test + fun benchQueryingEmptyReturnByIds() { + benchmarkRule.measureRepeated { + db.query(Filter(ids = listOf("176d972f60dcdc3212ed8c92ef85065c176d972f60dcdc3212ed8c92ef85065c"))) + } + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt new file mode 100644 index 0000000000..628f24c343 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableTest.kt @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.test.core.app.ApplicationProvider +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import junit.framework.TestCase +import junit.framework.TestCase.fail +import org.junit.After +import org.junit.Before +import org.junit.Test + +class AddressableTest { + private lateinit var db: EventStore + + val signer = NostrSignerSync() + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = EventStore(context, "test.db", relayUrl = "testUrl") + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testReplacingAddressables() { + val time = TimeUtils.now() + val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + + db.insert(version1) + + db.assertQuery(version1, Filter(ids = listOf(version1.id))) + + db.insert(version2) + + db.assertQuery(null, Filter(ids = listOf(version1.id))) + db.assertQuery(version2, Filter(ids = listOf(version2.id))) + + db.insert(version3) + + db.assertQuery(null, Filter(ids = listOf(version1.id))) + db.assertQuery(null, Filter(ids = listOf(version2.id))) + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + } + + @Test + fun testBlockingOldAddressables() { + val time = TimeUtils.now() + val version1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val version2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val version3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + + db.insert(version3) + + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + + try { + db.insert(version2) + fail("It should not allow inserting an older version") + } catch (e: Exception) { + TestCase.assertTrue(e is SQLiteConstraintException) + } + + try { + db.insert(version1) + fail("It should not allow inserting an older version") + } catch (e: Exception) { + TestCase.assertTrue(e is SQLiteConstraintException) + } + + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + db.assertQuery(null, Filter(ids = listOf(version2.id))) + db.assertQuery(null, Filter(ids = listOf(version1.id))) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AssertUtils.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AssertUtils.kt new file mode 100644 index 0000000000..6dee855808 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AssertUtils.kt @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import junit.framework.TestCase + +fun EventStore.assertQuery( + expected: Event?, + filter: Filter, +) { + val queryResult = query(filter) + val countResult = count(filter) + if (expected == null) { + TestCase.assertEquals(0, queryResult.size) + TestCase.assertEquals(0, countResult) + } else { + TestCase.assertEquals(1, queryResult.size) + TestCase.assertEquals(1, countResult) + TestCase.assertEquals(expected.toJson(), queryResult.first().toJson()) + } +} + +fun EventStore.assertQuery( + expected: List, + filter: Filter, +) { + val queryResult = query(filter) + val countResult = count(filter) + TestCase.assertEquals(expected.size, queryResult.size) + TestCase.assertEquals(expected.size, countResult) + expected.forEachIndexed { index, event -> + TestCase.assertEquals(event.toJson(), queryResult[index].toJson()) + } +} + +fun SQLiteEventStore.assertQuery( + expected: Event?, + filter: Filter, +) { + val queryResult = query(filter) + val countResult = count(filter) + if (expected == null) { + TestCase.assertEquals(0, queryResult.size) + TestCase.assertEquals(0, countResult) + } else { + TestCase.assertEquals(1, queryResult.size) + TestCase.assertEquals(1, countResult) + TestCase.assertEquals(expected.toJson(), queryResult.first().toJson()) + } +} + +fun SQLiteEventStore.assertQuery( + expected: List, + filter: Filter, +) { + val queryResult = query(filter) + val countResult = count(filter) + TestCase.assertEquals(expected.size, queryResult.size) + TestCase.assertEquals(expected.size, countResult) + expected.forEachIndexed { index, event -> + TestCase.assertEquals(event.toJson(), queryResult[index].toJson()) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt new file mode 100644 index 0000000000..8aa2a7f3be --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/BasicTest.kt @@ -0,0 +1,207 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import junit.framework.TestCase.assertEquals +import org.junit.After +import org.junit.Before +import org.junit.Test + +class BasicTest { + private lateinit var db: SQLiteEventStore + + val signer = NostrSignerSync() + + companion object Companion { + val profile = + MetadataEvent( + id = "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe", + pubKey = "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67", + createdAt = 1740669816, + tags = + arrayOf( + arrayOf("alt", "User profile for Vitor"), + arrayOf("name", "Vitor"), + ), + content = "{\"name\":\"Vitor\"}", + sig = "977a6152199f17d103d8d56736ed1b7767054464cf9423d017c01c8cdd2344698f0a5e13da8dff98d01bb1f798837e3b6271e1fd1cac861bb90686f622ae6ef4", + ) + + val comment = + CommentEvent( + id = "fecb2ecf61a1433d417a784d10bd1e8ec19a916170a53ca8fb3a15fc666a6592", + pubKey = "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a", + createdAt = 1747753115, + tags = + arrayOf( + arrayOf("alt", "Reply to geo:drt3n"), + arrayOf("I", "geo:drt3n"), + arrayOf("I", "geo:drt3"), + arrayOf("I", "geo:drt"), + arrayOf("I", "geo:dr"), + arrayOf("I", "geo:d"), + arrayOf("K", "geo"), + arrayOf("i", "geo:drt3n"), + arrayOf("i", "geo:drt3"), + arrayOf("i", "geo:drt"), + arrayOf("i", "geo:dr"), + arrayOf("i", "geo:d"), + arrayOf("k", "geo"), + ), + content = "testing", + sig = "12070e663272f1227c639fb834eb2122fc7bb995f4c49e55ebb1dfe2135ef7347d44810bacd2e64fd26b8826fd47d2800ce6c3d3b579bb3afe39088ffd4faa60", + ) + } + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = SQLiteEventStore(context, null) + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertDeleteEvent() { + val note = signer.sign(TextNoteEvent.build("test1")) + + db.insertEvent(note) + + db.assertQuery(note, Filter(ids = listOf(note.id))) + + db.delete(note.id) + + db.assertQuery(null, Filter(ids = listOf(note.id))) + + db.insertEvent(note) + + db.assertQuery(note, Filter(ids = listOf(note.id))) + } + + @Test + fun testEmptyFilter() { + val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1)) + val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2)) + + db.insertEvent(note1) + + db.assertQuery(note1, Filter()) + + db.insertEvent(note2) + + db.assertQuery(listOf(note2, note1), Filter()) + } + + @Test + fun testLimitFilter() { + val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = 1)) + val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = 2)) + val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = 3)) + val note4 = signer.sign(TextNoteEvent.build("test4", createdAt = 4)) + + db.insertEvent(note1) + + db.assertQuery(note1, Filter(limit = 1)) + + db.insertEvent(note2) + db.insertEvent(note3) + db.insertEvent(note4) + + db.assertQuery(listOf(note4), Filter(limit = 1)) + } + + @Test + fun testPubkeyTag() { + db.insertEvent(comment) + db.insertEvent(profile) + + db.assertQuery( + comment, + Filter(authors = listOf(comment.pubKey), tags = mapOf("I" to listOf("geo:drt3n"))), + ) + } + + @Test + fun testTagOnly() { + db.insertEvent(comment) + db.insertEvent(profile) + + db.assertQuery(comment, Filter(tags = mapOf("I" to listOf("geo:drt3n")))) + } + + @Test + fun testTagWithSinceOnly() { + db.insertEvent(comment) + db.insertEvent(profile) + + db.assertQuery( + comment, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt - 1), + ) + db.assertQuery( + comment, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt), + ) + db.assertQuery( + null, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), since = comment.createdAt + 1), + ) + } + + @Test + fun testTagWithUntilOnly() { + db.insertEvent(comment) + db.insertEvent(profile) + + db.assertQuery( + null, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt - 1), + ) + db.assertQuery( + comment, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt), + ) + db.assertQuery( + comment, + Filter(tags = mapOf("I" to listOf("geo:drt3n")), until = comment.createdAt + 1), + ) + } + + @Test + fun testTagWithUntilOnlyEmitting() { + db.insertEvent(comment) + db.insertEvent(profile) + + db.query(Filter(tags = mapOf("I" to listOf("geo:drt3n")))) { event -> + assertEquals(comment.toJson(), event.toJson()) + } + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt new file mode 100644 index 0000000000..fde0d95665 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionTest.kt @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.test.core.app.ApplicationProvider +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import junit.framework.TestCase.fail +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class DeletionTest { + private lateinit var db: EventStore + + val signer = NostrSignerSync() + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = EventStore(context, null) + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertDeleteEvent() { + val note1 = signer.sign(TextNoteEvent.build("test1")) + val note2 = signer.sign(TextNoteEvent.build("test2")) + val note3 = signer.sign(TextNoteEvent.build("test3")) + + db.insert(note1) + db.insert(note2) + db.insert(note3) + + db.assertQuery(note1, Filter(ids = listOf(note1.id))) + db.assertQuery(note2, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + val deletion = signer.sign(DeletionEvent.build(listOf(note1))) + + db.insert(deletion) + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(note2, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + // trying to insert again should fail. + try { + db.insert(note1) + fail("Should not be able to insert a deleted event") + } catch (e: SQLiteConstraintException) { + assertEquals("blocked: a deletion event for this event id exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + } + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(note2, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + } + + @Test + fun testInsertDeleteEventOfAddressable() { + val time = TimeUtils.now() + val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + + db.insert(note1) + + db.assertQuery(note1, Filter(ids = listOf(note1.id))) + + db.insert(note2) + + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(note2, Filter(ids = listOf(note2.id))) + + db.insert(note3) + + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + val deletion = signer.sign(DeletionEvent.build(listOf(note1))) + + db.insert(deletion) + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(null, Filter(ids = listOf(note3.id))) + + // trying to insert again should fail. + try { + db.insert(note1) + fail("Should not be able to insert a deleted event") + } catch (e: SQLiteConstraintException) { + assertEquals("blocked: a deletion event for this event id exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + } + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(null, Filter(ids = listOf(note3.id))) + } + + @Test + fun testInsertDeleteEventOfAddressable2() { + val time = TimeUtils.now() + val note1 = signer.sign(LongTextNoteEvent.build("my cool blog, version 1", "title", dTag = "my-cool-blog", createdAt = time)) + val note2 = signer.sign(LongTextNoteEvent.build("my cool blog, version 2", "title", dTag = "my-cool-blog", createdAt = time + 1)) + val note3 = signer.sign(LongTextNoteEvent.build("my cool blog, version 3", "title", dTag = "my-cool-blog", createdAt = time + 2)) + + db.insert(note1) + db.insert(note2) + db.insert(note3) + + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + val deletion = signer.sign(DeletionEvent.buildAddressOnly(listOf(note1))) + + db.insert(deletion) + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(null, Filter(ids = listOf(note3.id))) + + // trying to insert again should fail. + try { + db.insert(note1) + fail("Should not be able to insert a deleted event") + } catch (e: SQLiteConstraintException) { + assertEquals("blocked: a deletion event for this address exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + } + + db.assertQuery(deletion, Filter(ids = listOf(deletion.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(null, Filter(ids = listOf(note3.id))) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationTest.kt new file mode 100644 index 0000000000..9194df913b --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationTest.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.test.core.app.ApplicationProvider +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration +import com.vitorpamplona.quartz.utils.TimeUtils +import junit.framework.TestCase.assertTrue +import junit.framework.TestCase.fail +import org.junit.After +import org.junit.Before +import org.junit.Test + +class ExpirationTest { + private lateinit var db: EventStore + + val signer = NostrSignerSync() + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = EventStore(context, null, relayUrl = "testUrl") + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testDeletingExpiredEvents() { + val time = TimeUtils.now() + + val noteSafe = + signer.sign( + TextNoteEvent.build("test1", createdAt = time + 1) { + expiration(time + 100) + }, + ) + + db.insert(noteSafe) + + val noteToExpire = + signer.sign( + TextNoteEvent.build("test1", createdAt = time + 1) { + expiration(time + 1) + }, + ) + + db.insert(noteToExpire) + + db.assertQuery(noteToExpire, Filter(ids = listOf(noteToExpire.id))) + + Thread.sleep(2000) + + db.deleteExpiredEvents() + + db.assertQuery(null, Filter(ids = listOf(noteToExpire.id))) + db.assertQuery(noteSafe, Filter(ids = listOf(noteSafe.id))) + } + + @Test + fun testInsertingExpiredEvents() { + val time = TimeUtils.now() + + val note1 = + signer.sign( + TextNoteEvent.build("test1", createdAt = time - 12) { + expiration(time - 10) + }, + ) + + try { + db.insert(note1) + fail("Should not be able to insert expired events") + } catch (e: Exception) { + assertTrue(e is SQLiteConstraintException) + } + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/LargeDBTests.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/LargeDBTests.kt new file mode 100644 index 0000000000..ac62d9ff33 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/LargeDBTests.kt @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import android.database.sqlite.SQLiteException +import android.util.Log +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip40Expiration.isExpired +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.util.zip.GZIPInputStream +import kotlin.system.measureTimeMillis + +@RunWith(AndroidJUnit4::class) +class LargeDBTests { + companion object { + fun getEventDB(): List { + // This file includes duplicates + val fullDBInputStream = getInstrumentation().context.assets.open("nostr_vitor_startup_data.json") + + return JsonMapper.mapper.readValue>( + GZIPInputStream(fullDBInputStream), + ) + } + + val events = getEventDB().distinctBy { it.id }.filter { !it.isExpired() }.sortedBy { it.createdAt } + } + + private lateinit var db: EventStore + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = EventStore(context, "largeDBTest.db") + db.store.clearDB() + } + + @After + fun tearDown() { + db.store.clearDB() + db.close() + } + + @Test + fun insertHeavyEvent() { + events.first { it.id == "3f34b8cb682307ec11753de4669ce8948e95fd6fb360d79136446c5547fd235e" }.let { event -> + println(event.toJson()) + try { + val measure = + measureTimeMillis { + db.insert(event) + } + if (measure > 1) { + println("Inserted event ${event.id} of kind ${event.kind} in $measure ms") + } + } catch (e: SQLiteException) { + Log.w("LargeDBTests", "Error inserting event: ${e.message} for event: ${event.toJson()}") + } + } + } + + @Test + fun insertDatabase() { + events.forEach { event -> + try { + val measure = + measureTimeMillis { + db.insert(event) + } + if (measure > 1) { + println("Inserted event ${event.id} of kind ${event.kind} in $measure ms") + } + } catch (e: SQLiteException) { + Log.w("LargeDBTests", "Error inserting event: ${e.message} for event: ${event.toJson()}") + } + } + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt new file mode 100644 index 0000000000..0a85cfc75c --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableTest.kt @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.test.core.app.ApplicationProvider +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.utils.TimeUtils +import junit.framework.TestCase +import junit.framework.TestCase.fail +import org.junit.After +import org.junit.Before +import org.junit.Test + +class ReplaceableTest { + private lateinit var db: EventStore + + val signer = NostrSignerSync() + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = EventStore(context, null, relayUrl = "testUrl") + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testReplacing() { + val time = TimeUtils.now() + val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time)) + val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1)) + val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2)) + + db.insert(version1) + + db.assertQuery(version1, Filter(ids = listOf(version1.id))) + + db.insert(version2) + + db.assertQuery(null, Filter(ids = listOf(version1.id))) + db.assertQuery(version2, Filter(ids = listOf(version2.id))) + + db.insert(version3) + + db.assertQuery(null, Filter(ids = listOf(version1.id))) + db.assertQuery(null, Filter(ids = listOf(version2.id))) + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + } + + @Test + fun testBlockingOldVersions() { + val time = TimeUtils.now() + val version1 = signer.sign(MetadataEvent.createNew("Vitor 1", createdAt = time)) + val version2 = signer.sign(MetadataEvent.createNew("Vitor 2", createdAt = time + 1)) + val version3 = signer.sign(MetadataEvent.createNew("Vitor 3", createdAt = time + 2)) + + db.insert(version3) + + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + + try { + db.insert(version2) + fail("It should not allow inserting an older version") + } catch (e: Exception) { + TestCase.assertTrue(e is SQLiteConstraintException) + } + + try { + db.insert(version1) + fail("It should not allow inserting an older version") + } catch (e: Exception) { + TestCase.assertTrue(e is SQLiteConstraintException) + } + + db.assertQuery(version3, Filter(ids = listOf(version3.id))) + db.assertQuery(null, Filter(ids = listOf(version2.id))) + db.assertQuery(null, Filter(ids = listOf(version1.id))) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishTest.kt new file mode 100644 index 0000000000..578d6aeb76 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishTest.kt @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import androidx.test.core.app.ApplicationProvider +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import junit.framework.TestCase.fail +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class RightToVanishTest { + private lateinit var db: EventStore + + val signer = NostrSignerSync() + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = EventStore(context, null, relayUrl = "testUrl") + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testInsertDeleteEvent() { + val time = TimeUtils.now() + val note1 = signer.sign(TextNoteEvent.build("test1", createdAt = time)) + val note2 = signer.sign(TextNoteEvent.build("test2", createdAt = time + 1)) + val note3 = signer.sign(TextNoteEvent.build("test3", createdAt = time + 2)) + + db.insert(note1) + db.insert(note2) + db.insert(note3) + + db.assertQuery(note1, Filter(ids = listOf(note1.id))) + db.assertQuery(note2, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + val vanish = signer.sign(RequestToVanishEvent.build("testUrl", createdAt = time + 2)) + + db.insert(vanish) + + db.assertQuery(vanish, Filter(ids = listOf(vanish.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + + // trying to insert again should fail. + try { + db.insert(note1) + fail("Should not be able to insert a deleted event") + } catch (e: SQLiteConstraintException) { + assertEquals("blocked: a request to vanish event exists (code 1811 SQLITE_CONSTRAINT_TRIGGER)", e.message) + } + + db.assertQuery(vanish, Filter(ids = listOf(vanish.id))) + db.assertQuery(null, Filter(ids = listOf(note1.id))) + db.assertQuery(null, Filter(ids = listOf(note2.id))) + db.assertQuery(note3, Filter(ids = listOf(note3.id))) + } +} diff --git a/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt new file mode 100644 index 0000000000..f73a3372d0 --- /dev/null +++ b/quartz/src/androidTest/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/SearchTest.kt @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import org.junit.After +import org.junit.Before +import org.junit.Test + +class SearchTest { + private lateinit var db: SQLiteEventStore + + companion object Companion { + val profile = + MetadataEvent( + id = "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe", + pubKey = "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67", + createdAt = 1740669816, + tags = + arrayOf( + arrayOf("alt", "User profile for Vitor"), + arrayOf("name", "Vitor"), + ), + content = "{\"name\":\"Vitor\"}", + sig = "977a6152199f17d103d8d56736ed1b7767054464cf9423d017c01c8cdd2344698f0a5e13da8dff98d01bb1f798837e3b6271e1fd1cac861bb90686f622ae6ef4", + ) + + val comment = + CommentEvent( + id = "fecb2ecf61a1433d417a784d10bd1e8ec19a916170a53ca8fb3a15fc666a6592", + pubKey = "f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a", + createdAt = 1747753115, + tags = + arrayOf( + arrayOf("alt", "Reply to geo:drt3n"), + arrayOf("I", "geo:drt3n"), + arrayOf("I", "geo:drt3"), + arrayOf("I", "geo:drt"), + arrayOf("I", "geo:dr"), + arrayOf("I", "geo:d"), + arrayOf("K", "geo"), + arrayOf("i", "geo:drt3n"), + arrayOf("i", "geo:drt3"), + arrayOf("i", "geo:drt"), + arrayOf("i", "geo:dr"), + arrayOf("i", "geo:d"), + arrayOf("k", "geo"), + ), + content = "testing", + sig = "12070e663272f1227c639fb834eb2122fc7bb995f4c49e55ebb1dfe2135ef7347d44810bacd2e64fd26b8826fd47d2800ce6c3d3b579bb3afe39088ffd4faa60", + ) + } + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + db = SQLiteEventStore(context, null) + } + + @After + fun tearDown() { + db.close() + } + + @Test + fun testTagWithSearch() { + db.insertEvent(comment) + db.insertEvent(profile) + + db.assertQuery(null, Filter(search = "testing1")) + db.assertQuery(comment, Filter(search = "testing")) + db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing")) + db.assertQuery(null, Filter(kinds = listOf(TextNoteEvent.KIND), search = "testing")) + + db.delete(comment.id) + + db.assertQuery(null, Filter(search = "testing")) + db.assertQuery(null, Filter(kinds = listOf(CommentEvent.KIND), search = "testing")) + + db.insertEvent(comment) + + db.assertQuery(comment, Filter(search = "testing")) + db.assertQuery(comment, Filter(kinds = listOf(CommentEvent.KIND), search = "testing")) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/README.md b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/README.md new file mode 100644 index 0000000000..c8084f0a29 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/README.md @@ -0,0 +1,193 @@ +# Event Store Module + +This module implements an **Event Store** with nostr-native queries. + +The goal was not to make the fastest database, since there could be multiple optimizations made if +consistency can be sacrificed, but a database that will never crash and never go corrupt. + +## Responsibilities + +- **Storage & Retrieval**: + Stores Nostr events and enables retrieval using Nostr filters + +- **Replaceable Events**: + - Old versions are removed when newer versions arrive. + - Old versions are blocked if newer versions exist. + +- **Ephemeral Events** + - Ephemeral events never stored. + +- **NIP-40 Expirations** + - Manages expiration timestamps and prunes expired events. + - Blocks expired events from being reinserted + +- **NIP-09 Deletion Events** + - Deletes by event id + - Deletes by address until the `created_at` + - Blocks deleted events from being re-inserted. + +- **NIP-62 Right to Vanish** + - Supports deleting an entire user until the `created_at` for enhanced privacy + +- **NIP-50 Full Text Search**: + - Implements content indexing and full text search supporting rich queries over event content. + +- **Immutable Tables** + Triggers ensure event immutability. + +## Indexing Strategy + +The store indexes events using five dedicated tables: +- `event_headers`: stores the canonical event fields. +- `event_tags`: indexes tag values for fast filtering on tag-based queries. +- `event_fts`: for the content of full text search +- `event_expirations`: to control when expired events must be deleted. +- `event_vanish`: to control up to when vanished accounts must be blocked. + +SQL triggers ensure the **immutability of stored events**, preventing accidental or intentional +modifications. + +## Querying + +This module supports optimized query planning, producing efficient SQL for multi-filter evaluation +across fields and tags, including `limit` clauses per filter: + +For instance, the following filter: +```kotlin +store.query( + listOf( + Filter(limit = 10), + Filter(authors = listOf(hexkey), kinds = listOf(1, 1111), search = "keywords", limit = 100), + Filter(kinds = listOf(20), search = "cats", limit = 30), + ) +) +``` + +Becomes + +```sql +SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers +INNER JOIN ( + SELECT event_headers.row_id AS row_id + FROM event_headers + ORDER BY created_at DESC,id ASC + LIMIT 10 + + UNION + + SELECT event_headers.row_id AS row_id + FROM event_headers + INNER JOIN event_fts ON event_fts.event_header_row_id = event_headers.row_id + WHERE event_headers.kind IN (1, 1111) + AND event_headers.pubkey = hexkey + AND event_fts MATCH "keywords" + ORDER BY created_at DESC, id ASC + LIMIT 100 + + UNION + + SELECT event_headers.row_id AS row_id + FROM event_headers + INNER JOIN event_fts ON event_fts.event_header_row_id = event_headers.row_id + WHERE event_headers.kind = 20 + AND event_fts MATCH "cats" + ORDER BY created_at DESC,id ASC + LIMIT 30 +) AS filtered ON event_headers.row_id = filtered.row_id +ORDER BY created_at DESC,id +``` + +The union operations support complex filter lists while avoiding redundant data fetching and +duplicated outstreams. + +## How to Use + +The `EventStore` class provides a high-level interface for interacting with the event database. +It is initialized with a `SQLiteDatabase` instance, and it manages the underlying tables and query planning. + +### Initialization + +To initialize the `EventStore` in your Application class: + +```kotlin +val eventStore = EventStore(context, "dbname.db", relayUrlIdentifier) +``` + +### Querying Events + +To query events, use the `query` method with one or more `Filter` objects: + +```kotlin +val filters = listOf( + Filter(limit = 10), + Filter(authors = listOf(hexkey)) +) + +val events = eventStore.query(filters) +``` + +or to receive events as the cursor sends: + +```kotlin +val filters = listOf( + Filter(limit = 10), + Filter(authors = listOf(hexkey)) +) + +eventStore.query(filters) { event -> + // do something +} +``` + +`count` and `delete` also accept one or more filters. + +### Inserting Events + +Insert a single event using the `insert` method: + +```kotlin +eventStore.insert(event) +``` + +### Deleting Events + +Events should be deleted by adding a DeletionRequest or a VanishRequest to the db, but to manually +delete an event by ID, use the `delete` method: + +```kotlin +eventStore.delete(event.id) +``` + +### Full-Text Search + +The store supports full-text search using the `search` parameter in filters: + +```kotlin +val result = eventStore.query(Filter(search = "bitcoin", limit = 20)) +``` + +This will match any event whose content contains "bitcoin", returning the most recent 20 results. + +### Periodic cleanup of expired events. + +The store exposes a `deleteExpiredEvents` to be used in a periodic clean up procedure. Users +should use a WorkManager or a coroutine to periodically call `store.deleteExpiredEvents()`. We +recommend a 15-minute window to remove recently expired events from the database. + +Here's an example of a Worker that should be added to your application class. + +```kotlin +class ExpirationWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) { + override fun doWork(): Result { + YourApplication.store.deleteExpiredEvents() + return Result.success() + } +} + +fun schedulePeriodicWork(context: Context) { + val periodicWorkRequest = + PeriodicWorkRequestBuilder(15, TimeUnit.MINUTES).build() + + WorkManager.getInstance(context).enqueue(periodicWorkRequest) +} +``` diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableModule.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableModule.kt new file mode 100644 index 0000000000..b89d2a14e2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/AddressableModule.kt @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.database.sqlite.SQLiteDatabase + +class AddressableModule { + fun create(db: SQLiteDatabase) { + db.execSQL( + """ + CREATE UNIQUE INDEX addressable_idx + ON event_headers (kind, pubkey, d_tag) + WHERE kind >= 30000 AND kind < 40000 + """.trimIndent(), + ) + + // Rejects old addressables + db.execSQL( + """ + CREATE TRIGGER reject_older_addressable_event + BEFORE INSERT ON event_headers + FOR EACH ROW + WHEN (NEW.kind >= 30000 AND NEW.kind < 40000) + BEGIN + -- Check for existing newer record + SELECT RAISE(ABORT, 'duplicate: A newer or equally new record already exists') + WHERE EXISTS ( + SELECT 1 FROM event_headers + WHERE + event_headers.created_at >= NEW.created_at AND + event_headers.kind = NEW.kind AND + event_headers.pubkey = NEW.pubkey AND + event_headers.d_tag = NEW.d_tag + ); + + DELETE FROM event_tags + WHERE event_header_row_id in ( + SELECT row_id FROM event_headers + WHERE + event_headers.kind = NEW.kind AND + event_headers.pubkey = NEW.pubkey AND + event_headers.d_tag = NEW.d_tag + ); + + DELETE FROM event_headers + WHERE + event_headers.kind = NEW.kind AND + event_headers.pubkey = NEW.pubkey AND + event_headers.d_tag = NEW.d_tag; + END; + """.trimIndent(), + ) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt new file mode 100644 index 0000000000..3a20e47cae --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/DeletionRequestModule.kt @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.database.sqlite.SQLiteDatabase +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent + +class DeletionRequestModule { + fun create(db: SQLiteDatabase) { + // rejects deleted events. + db.execSQL( + """ + CREATE TRIGGER reject_deleted_events + BEFORE INSERT ON event_headers + FOR EACH ROW + BEGIN + -- Check for ID-based deletion record + SELECT RAISE(ABORT, 'blocked: a deletion event for this event id exists') + WHERE EXISTS ( + SELECT 1 FROM event_headers INNER JOIN event_tags ON event_headers.row_id = event_tags.event_header_row_id + WHERE + event_headers.created_at >= NEW.created_at AND + event_headers.kind = 5 AND + event_headers.pubkey = NEW.pubkey AND + event_tags.tag_name = 'e' AND + event_tags.tag_value = NEW.id + ); + + -- Check for address-based deletion record + SELECT RAISE(ABORT, 'blocked: a deletion event for this address exists') + WHERE EXISTS ( + SELECT 1 FROM event_headers INNER JOIN event_tags ON event_headers.row_id = event_tags.event_header_row_id + WHERE + event_headers.created_at >= NEW.created_at AND + event_headers.kind = 5 AND + event_headers.pubkey = NEW.pubkey AND + event_tags.tag_name = 'a' AND + event_tags.tag_value = NEW.kind || ':' || NEW.pubkey || ':' || NEW.d_tag + ); + END; + """.trimIndent(), + ) + } + + fun insert( + event: Event, + headerId: Long, + db: SQLiteDatabase, + ) { + if (event is DeletionEvent) { + val idValues = event.deleteEventIds() + val idParams = idValues.joinToString(",") { "?" } + + val addresses = event.deleteAddresses() + val addressParams = addresses.joinToString(",") { "(?, ?)" } + val addressValues = addresses.flatMap { listOf(it.kind, it.dTag) } + + val whereClause = + if (idValues.isNotEmpty() && addresses.isNotEmpty()) { + "(id IN ($idParams) OR (kind, d_tag) IN ($addressParams)) AND pubkey = ?" + } else if (idValues.isNotEmpty()) { + "id IN ($idParams) AND pubkey = ?" + } else if (addresses.isNotEmpty()) { + "(kind, d_tag) IN ($addressParams) AND pubkey = ?" + } else { + return + } + val whereParams = idValues.plus(addressValues).plus(event.pubKey).toTypedArray() + + db.execSQL( + """ + DELETE FROM event_headers + WHERE $whereClause; + """.trimIndent(), + whereParams, + ) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EphemeralModule.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EphemeralModule.kt new file mode 100644 index 0000000000..011f114e01 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EphemeralModule.kt @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.database.sqlite.SQLiteDatabase + +class EphemeralModule { + fun create(db: SQLiteDatabase) { + // Rejects all ephemeral events. + db.execSQL( + """ + CREATE TRIGGER reject_ephemeral_events + BEFORE INSERT ON event_headers + FOR EACH ROW + WHEN (NEW.kind >= 20000 AND NEW.kind < 30000) + BEGIN + SELECT RAISE(ABORT, 'blocked: cannot store ephemeral events'); + END; + """.trimIndent(), + ) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt new file mode 100644 index 0000000000..d1a2e2436e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventIndexesModule.kt @@ -0,0 +1,469 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.EventFactory +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.store.sqlite.sql.where + +class EventIndexesModule( + val fts: FullTextSearchModule, +) { + fun create(db: SQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE event_headers ( + row_id INTEGER PRIMARY KEY AUTOINCREMENT, + id TEXT NOT NULL, + pubkey TEXT NOT NULL, + created_at INTEGER NOT NULL, + kind INTEGER NOT NULL, + d_tag TEXT, + tags TEXT NOT NULL, + content TEXT NOT NULL, + sig TEXT NOT NULL + ) + """.trimIndent(), + ) + + db.execSQL( + """ + CREATE TABLE event_tags ( + event_header_row_id INTEGER, + tag_name TEXT NOT NULL, + tag_value TEXT NOT NULL, + FOREIGN KEY (event_header_row_id) REFERENCES event_headers(row_id) ON DELETE CASCADE + ) + """.trimIndent(), + ) + + db.execSQL("CREATE UNIQUE INDEX event_headers_id ON event_headers (id)") + db.execSQL("CREATE INDEX query_by_kind_pubkey_idx ON event_headers (created_at desc, kind, pubkey, d_tag)") + db.execSQL("CREATE INDEX query_by_id_idx ON event_headers (created_at desc, id)") + db.execSQL("CREATE INDEX query_by_tags_idx ON event_tags (tag_name, tag_value)") + + // Prevent updates to maintain immutability + db.execSQL( + """ + CREATE TRIGGER event_headers_prevent_update + BEFORE UPDATE ON event_headers + FOR EACH ROW + BEGIN + SELECT RAISE(ABORT, 'Error: Updates are not allowed.'); + END; + """.trimIndent(), + ) + + db.execSQL( + """ + CREATE TRIGGER event_tags_prevent_update + BEFORE UPDATE ON event_tags + FOR EACH ROW + BEGIN + SELECT RAISE(ABORT, 'Error: Updates are not allowed.'); + END; + """.trimIndent(), + ) + } + + val sqlInsertHeader = + """ + INSERT INTO event_headers + (id, pubkey, created_at, kind, tags, content, sig, d_tag) + VALUES + (?, ?, ?, ?, ?, ?, ?, ?) + """.trimIndent() + + val sqlInsertTags = + """ + INSERT OR ROLLBACK INTO event_tags + (event_header_row_id, tag_name, tag_value) + VALUES + (?,?,?) + """.trimIndent() + + fun insert( + event: Event, + db: SQLiteDatabase, + ): Long { + val stmt = db.compileStatement(sqlInsertHeader) + stmt.bindString(1, event.id) + stmt.bindString(2, event.pubKey) + stmt.bindLong(3, event.createdAt) + stmt.bindLong(4, event.kind.toLong()) + stmt.bindString(5, JsonMapper.mapper.writeValueAsString(event.tags)) + stmt.bindString(6, event.content) + stmt.bindString(7, event.sig) + if (event is AddressableEvent) { + stmt.bindString(8, event.dTag()) + } else { + stmt.bindNull(8) + } + val headerId = stmt.executeInsert() + + val tagsToIndex = event.indexableTags() + + if (tagsToIndex.isNotEmpty()) { + val sql = + buildString { + append(sqlInsertTags) + repeat(tagsToIndex.size - 1) { + append(",(?,?,?)") + } + } + + val stmtTags = db.compileStatement(sql) + + var index = 1 + tagsToIndex.forEach { tag -> + stmtTags.bindLong(index++, headerId) + stmtTags.bindString(index++, tag[0]) + stmtTags.bindString(index++, tag[1]) + } + + stmtTags.executeInsert() + } + + return headerId + } + + fun Event.indexableTags(): List { + val indexableTagNames = extraIndexableTagNames() + return if (indexableTagNames.isNotEmpty()) { + tags.filter { it.size >= 2 && (it[0].length == 1 || it[0] in indexableTagNames) } + } else { + tags.filter { it.size >= 2 && it[0].length == 1 } + } + } + + fun planQuery(filter: Filter): String { + val rowIdSubQuery = prepareRowIDSubQueries(filter) ?: return makeEverythingQuery() + + return makeQueryIn(rowIdSubQuery.sql) + } + + fun query( + filter: Filter, + db: SQLiteDatabase, + ): List { + val rowIdSubQuery = prepareRowIDSubQueries(filter) ?: return db.runQuery(makeEverythingQuery()) + + return db.runQuery(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args) + } + + fun query( + filter: Filter, + db: SQLiteDatabase, + onEach: (Event) -> Unit, + ) { + val rowIdSubQuery = prepareRowIDSubQueries(filter) ?: return db.runQueryEmitting(makeEverythingQuery(), onEach = onEach) + + db.runQueryEmitting(makeQueryIn(rowIdSubQuery.sql), rowIdSubQuery.args, onEach) + } + + fun planQuery(filters: List): String { + val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it) } + if (rowIdSubQueries.isEmpty()) return makeEverythingQuery() + val unions = rowIdSubQueries.joinToString(" UNION ") { it.sql } + return makeQueryIn(unions) + } + + fun query( + filters: List, + db: SQLiteDatabase, + ): List { + val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it) } + + if (rowIdSubQueries.isEmpty()) return db.runQuery(makeEverythingQuery()) + + val unions = rowIdSubQueries.joinToString(" UNION ") { it.sql } + val args = rowIdSubQueries.flatMap { it.args } + + return db.runQuery(makeQueryIn(unions), args) + } + + fun query( + filters: List, + db: SQLiteDatabase, + onEach: (Event) -> Unit, + ) { + val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it) } + + if (rowIdSubQueries.isEmpty()) return db.runQueryEmitting(makeEverythingQuery(), onEach = onEach) + + val unions = rowIdSubQueries.joinToString(" UNION ") { it.sql } + val args = rowIdSubQueries.flatMap { it.args } + + db.runQueryEmitting(makeQueryIn(unions), args, onEach) + } + + private fun makeEverythingQuery() = "SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC, id" + + private fun makeQueryIn(rowIdQuery: String) = + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN ($rowIdQuery) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + """.trimIndent() + + private fun SQLiteDatabase.runQuery( + sql: String, + args: List = emptyList(), + ): List = + rawQuery(sql, args.toTypedArray()).use { cursor -> + parseResults(cursor) + } + + private fun parseResults(cursor: Cursor): List { + val events = ArrayList() + + while (cursor.moveToNext()) { + events.add( + EventFactory.create( + cursor.getString(0).intern(), + cursor.getString(1).intern(), + cursor.getLong(2), + cursor.getInt(3), + JsonMapper.mapper.readValue>>(cursor.getString(4)), + cursor.getString(5).intern(), + cursor.getString(6).intern(), + ), + ) + } + + return events + } + + private fun SQLiteDatabase.runQueryEmitting( + sql: String, + args: List = emptyList(), + onEach: (Event) -> Unit, + ) = rawQuery(sql, args.toTypedArray()).use { cursor -> + emitResults(cursor, onEach) + } + + private fun emitResults( + cursor: Cursor, + onEach: (Event) -> Unit, + ) { + while (cursor.moveToNext()) { + onEach( + EventFactory.create( + cursor.getString(0).intern(), + cursor.getString(1).intern(), + cursor.getLong(2), + cursor.getInt(3), + JsonMapper.mapper.readValue>>(cursor.getString(4)), + cursor.getString(5).intern(), + cursor.getString(6).intern(), + ), + ) + } + } + + // -------------- + // Counts + // ------------- + fun count( + filter: Filter, + db: SQLiteDatabase, + ): Int { + val rowIdSubQuery = prepareRowIDSubQueries(filter) ?: return db.countEverything() + + return db.countIn(rowIdSubQuery.sql, rowIdSubQuery.args) + } + + fun count( + filters: List, + db: SQLiteDatabase, + ): Int { + val rowIdSubQueries = filters.mapNotNull { prepareRowIDSubQueries(it) } + + if (rowIdSubQueries.isEmpty()) return db.countEverything() + + val unions = rowIdSubQueries.joinToString(" UNION ") { it.sql } + val args = rowIdSubQueries.flatMap { it.args } + + return db.countIn(unions, args) + } + + private fun SQLiteDatabase.countEverything() = runCount("SELECT count(*) as count FROM event_headers") + + private fun SQLiteDatabase.countIn( + rowIdQuery: String, + args: List, + ) = runCount("SELECT COUNT(*) as count FROM ($rowIdQuery)", args) + + private fun SQLiteDatabase.runCount( + sql: String, + args: List = emptyList(), + ): Int = + rawQuery(sql, args.toTypedArray()).use { cursor -> + cursor.moveToNext() + cursor.getInt(0) + } + + // -------------- + // Deletes + // ------------- + fun delete( + filter: Filter, + db: SQLiteDatabase, + ): Int? { + val rowIdQuery = prepareRowIDSubQueries(filter) ?: return null + return db.runDelete(rowIdQuery.sql, rowIdQuery.args) + } + + fun delete( + filters: List, + db: SQLiteDatabase, + ): Int? { + val rowIdSubqueries = filters.mapNotNull { prepareRowIDSubQueries(it) } + + if (rowIdSubqueries.isEmpty()) return null + + val unions = rowIdSubqueries.joinToString(" UNION ") { it.sql } + val args = rowIdSubqueries.flatMap { it.args } + + return db.runDelete(unions, args) + } + + private fun SQLiteDatabase.runDelete( + sql: String, + args: List = emptyList(), + ): Int = delete("event_headers", "row_id IN ($sql)", args.toTypedArray()) + + // ---------------------------- + // Inner row id selections + // ---------------------------- + fun prepareRowIDSubQueries(filter: Filter): RowIdSubQuery? { + if (!filter.isFilledFilter()) return null + + val hasHeaders = + with(filter) { + (ids != null && ids.isNotEmpty()) || + (authors != null && authors.isNotEmpty()) || + (kinds != null && kinds.isNotEmpty()) || + (since != null) || + (until != null) || + (tags != null && tags.containsKey("d")) + } + + val hasSearch = (filter.search != null && filter.search.isNotBlank()) + + val projection = + buildString { + val anchorColumn: String + val joins = mutableListOf() + + if (hasHeaders) { + append("SELECT event_headers.row_id as row_id FROM event_headers") + anchorColumn = "event_headers.row_id" + + if (hasSearch) { + joins.add("INNER JOIN ${fts.tableName} ON ${fts.tableName}.${fts.eventHeaderRowIdName} = $anchorColumn") + } + + filter.tags?.forEach { (tagName, _) -> + if (tagName != "d") { + joins.add("INNER JOIN event_tags as tag$tagName ON tag$tagName.event_header_row_id = $anchorColumn") + } + } + } else if (hasSearch) { + append("SELECT ${fts.tableName}.${fts.eventHeaderRowIdName} as row_id FROM ${fts.tableName}") + anchorColumn = "${fts.tableName}.${fts.eventHeaderRowIdName}" + + filter.tags?.forEach { (tagName, _) -> + if (tagName != "d") { + joins.add("INNER JOIN event_tags as tag$tagName ON tag$tagName.event_header_row_id = $anchorColumn") + } + } + } else { + // has only tags + filter.tags?.forEach { (tagName, _) -> + if (tagName != "d") { + if (isEmpty()) { + append("SELECT tag$tagName.event_header_row_id as row_id FROM event_tags as tag$tagName") + } else { + joins.add("INNER JOIN event_tags as tag$tagName ON tag$tagName.event_header_row_id = tag${tagName.takeLast(1)}.event_header_row_id") + } + } + } + + if (isEmpty()) { + // only limit is present + append("SELECT event_headers.row_id as row_id FROM event_headers") + } + } + + if (joins.isNotEmpty()) { + append(" ${joins.joinToString(" ")}") + } + } + + val clause = + where { + filter.ids?.let { equalsOrIn("event_headers.id", it) } + filter.kinds?.let { equalsOrIn("event_headers.kind", it) } + filter.authors?.let { equalsOrIn("event_headers.pubkey", it) } + filter.since?.let { greaterThanOrEquals("event_headers.created_at", it) } + filter.until?.let { lessThanOrEquals("event_headers.created_at", it) } + + filter.tags?.forEach { (tagName, tagValues) -> + if (tagName == "d") { + equalsOrIn("event_headers.d_tag", tagValues) + } else { + equals("tag$tagName.tag_name", tagName) + equalsOrIn("tag$tagName.tag_value", tagValues) + } + } + + filter.search?.let { match(fts.tableName, it) } + } + + val whereClause = + if (filter.limit != null) { + "${clause.conditions} ORDER BY created_at DESC, id ASC LIMIT ${filter.limit}" + } else { + clause.conditions + } + + return RowIdSubQuery("$projection WHERE $whereClause", clause.args) + } + + fun deleteAll(db: SQLiteDatabase) { + db.execSQL("DELETE FROM event_tags") + db.execSQL("DELETE FROM event_headers") + } + + class RowIdSubQuery( + val sql: String, + val args: List, + ) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt new file mode 100644 index 0000000000..4794e4c7b4 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventStore.kt @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter + +class EventStore( + context: Context, + dbName: String? = "events.db", + val relayUrl: String? = "wss://quartz.local", +) { + val store = SQLiteEventStore(context, dbName, relayUrl) + + fun insert(event: Event) = store.insertEvent(event) + + fun query(filter: Filter) = store.query(filter) + + fun query(filters: List) = store.query(filters) + + fun query( + filter: Filter, + onEach: (Event) -> Unit, + ) = store.query(filter, onEach) + + fun query( + filters: List, + onEach: (Event) -> Unit, + ) = store.query(filters, onEach) + + fun count(filter: Filter) = store.count(filter) + + fun count(filters: List) = store.count(filters) + + fun delete(filter: Filter) = store.delete(filter) + + fun delete(filters: List) = store.delete(filters) + + fun deleteExpiredEvents() = store.deleteExpiredEvents() + + fun close() = store.close() +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationModule.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationModule.kt new file mode 100644 index 0000000000..39f738712a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ExpirationModule.kt @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.database.sqlite.SQLiteDatabase +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip40Expiration.expiration + +class ExpirationModule { + fun create(db: SQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE event_expirations ( + event_header_row_id INTEGER, + expiration INTEGER NOT NULL, + FOREIGN KEY (event_header_row_id) REFERENCES event_headers(row_id) ON DELETE CASCADE + ) + """.trimIndent(), + ) + + // Rejects old addressables + db.execSQL( + """ + CREATE TRIGGER reject_expired_events + BEFORE INSERT ON event_expirations + FOR EACH ROW + BEGIN + -- Check for existing newer record + SELECT RAISE(ABORT, 'blocked: this event is expired') + WHERE NEW.expiration <= unixepoch(); + END; + """.trimIndent(), + ) + + db.execSQL("CREATE UNIQUE INDEX events_exp_id ON event_expirations (event_header_row_id)") + } + + val insertExpiration = + """ + INSERT OR ROLLBACK INTO event_expirations (event_header_row_id, expiration) + VALUES (?, ?) + """.trimIndent() + + fun insert( + event: Event, + headerId: Long, + db: SQLiteDatabase, + ) { + val exp = event.expiration() + if (exp != null && exp > 0) { + val stmt = StatementCache.get(insertExpiration, db) + stmt.bindLong(1, headerId) + stmt.bindLong(2, exp) + stmt.executeInsert() + } + } + + val deleteExpiredEvents = + """ + DELETE FROM event_headers + WHERE row_id IN ( + SELECT event_expirations.event_header_row_id FROM event_expirations + WHERE event_expirations.expiration < unixepoch() + ); + """.trimIndent() + + fun deleteExpiredEvents(db: SQLiteDatabase) { + StatementCache.get(deleteExpiredEvents, db).execute() + } + + fun deleteAll(db: SQLiteDatabase) { + db.execSQL("DELETE FROM event_expirations") + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/FullTextSearchModule.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/FullTextSearchModule.kt new file mode 100644 index 0000000000..855d9cb99a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/FullTextSearchModule.kt @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteException +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip50Search.SearchableEvent + +class FullTextSearchModule { + val tableName = "event_fts" + val eventHeaderRowIdName = "event_header_row_id" + val contentName = "content" + + fun create(db: SQLiteDatabase) { + val ftsVersion = FullTextSearchModule().versionFinder(db) + db.execSQL( + """ + CREATE VIRTUAL TABLE $tableName + USING fts$ftsVersion($eventHeaderRowIdName, $contentName) + """, + ) + + // Foreign key cleanup for full text search + db.execSQL( + """ + CREATE TRIGGER fts_foreign_key + AFTER DELETE ON event_headers + FOR EACH ROW + BEGIN + DELETE FROM $tableName + WHERE old.row_id = $tableName.$eventHeaderRowIdName; + END; + """, + ) + } + + val insertFTS = + """ + INSERT OR ROLLBACK INTO $tableName ($eventHeaderRowIdName, $contentName) + VALUES (?, ?) + """.trimIndent() + + fun insert( + event: Event, + headerId: Long, + db: SQLiteDatabase, + ) { + if (event is SearchableEvent) { + val stmt = StatementCache.get(insertFTS, db) + stmt.bindLong(1, headerId) + stmt.bindString(2, event.indexableContent()) + stmt.executeInsert() + } + } + + fun versionFinder(db: SQLiteDatabase): Int = + try { + try { + db.execSQL("CREATE VIRTUAL TABLE dummy_fts5 USING fts5(dummy)") + 5 + } catch (e: SQLiteException) { + db.execSQL("CREATE VIRTUAL TABLE dummy_fts4 USING fts4(dummy)") + 4 + } + } catch (e: SQLiteException) { + db.execSQL("CREATE VIRTUAL TABLE dummy_fts3 USING fts3(dummy)") + 3 + } + + fun deleteAll(db: SQLiteDatabase) { + db.execSQL("DELETE FROM event_fts") + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableModule.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableModule.kt new file mode 100644 index 0000000000..688aff89d2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/ReplaceableModule.kt @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.database.sqlite.SQLiteDatabase + +class ReplaceableModule { + fun create(db: SQLiteDatabase) { + db.execSQL( + """ + CREATE UNIQUE INDEX replaceable_idx + ON event_headers (kind, pubkey) + WHERE (kind >= 10000 AND kind < 20000) OR (kind IN (0, 3)) + """.trimIndent(), + ) + + // Rejects old replaceables + db.execSQL( + """ + CREATE TRIGGER reject_older_replaceable_event + BEFORE INSERT ON event_headers + FOR EACH ROW + WHEN (NEW.kind >= 10000 AND NEW.kind < 20000) OR (NEW.kind IN (0, 3)) + BEGIN + -- Check for existing newer record + SELECT RAISE(ABORT, 'duplicate: A newer or equally new record already exists') + WHERE EXISTS ( + SELECT 1 FROM event_headers + WHERE + event_headers.created_at >= NEW.created_at AND + event_headers.kind = NEW.kind AND + event_headers.pubkey = NEW.pubkey + ); + + DELETE FROM event_tags + WHERE event_header_row_id in ( + SELECT row_id FROM event_headers + WHERE + event_headers.kind = NEW.kind AND + event_headers.pubkey = NEW.pubkey + ); + + -- Delete older records if this is the newest + DELETE FROM event_headers + WHERE + event_headers.kind = NEW.kind AND + event_headers.pubkey = NEW.pubkey; + END; + """.trimIndent(), + ) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishModule.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishModule.kt new file mode 100644 index 0000000000..715caeaba7 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/RightToVanishModule.kt @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.database.sqlite.SQLiteDatabase +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent + +class RightToVanishModule { + fun create(db: SQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE event_vanish ( + event_header_row_id INTEGER, + pubkey TEXT NOT NULL, + created_at INTEGER NOT NULL, + FOREIGN KEY (event_header_row_id) REFERENCES event_headers(row_id) ON DELETE CASCADE + ) + """.trimIndent(), + ) + + db.execSQL("CREATE UNIQUE INDEX event_vanish_key ON event_vanish (pubkey)") + + db.execSQL( + """ + CREATE TRIGGER delete_older_event_vanish + BEFORE INSERT ON event_vanish + FOR EACH ROW + BEGIN + -- Delete older records if this is the newest + DELETE FROM event_vanish + WHERE + event_vanish.created_at < NEW.created_at AND + event_vanish.pubkey = NEW.pubkey; + END; + """.trimIndent(), + ) + + db.execSQL( + """ + CREATE TRIGGER delete_events_on_event_vanish + AFTER INSERT ON event_vanish + FOR EACH ROW + BEGIN + DELETE FROM event_headers WHERE created_at < NEW.created_at AND pubkey = NEW.pubkey; + END; + """.trimIndent(), + ) + + // reject new events inside a right to vanish request + db.execSQL( + """ + CREATE TRIGGER reject_events_on_event_vanish + BEFORE INSERT ON event_headers + FOR EACH ROW + BEGIN + SELECT RAISE(ABORT, 'blocked: a request to vanish event exists') + WHERE EXISTS ( + SELECT 1 FROM event_vanish + WHERE + event_vanish.created_at >= NEW.created_at AND + event_vanish.pubkey = NEW.pubkey + ); + END; + """.trimIndent(), + ) + } + + val insertRTV = + """ + INSERT OR ROLLBACK INTO event_vanish (event_header_row_id, pubkey, created_at) + VALUES (?, ?, ?) + """.trimIndent() + + fun insert( + event: Event, + relayUrl: String?, + headerId: Long, + db: SQLiteDatabase, + ) { + if (event is RequestToVanishEvent && event.shouldVanishFrom(relayUrl)) { + val stmt = StatementCache.get(insertRTV, db) + stmt.bindLong(1, headerId) + stmt.bindString(2, event.pubKey) + stmt.bindLong(3, event.createdAt) + stmt.executeInsert() + } + } + + fun deleteAll(db: SQLiteDatabase) { + db.execSQL("DELETE FROM event_vanish") + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt new file mode 100644 index 0000000000..3d80a48988 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/SQLiteEventStore.kt @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.content.Context +import android.database.sqlite.SQLiteConstraintException +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper +import androidx.core.database.sqlite.transaction +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.isEphemeral +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip40Expiration.isExpired + +class SQLiteEventStore( + context: Context, + dbName: String? = "events.db", + val relayUrl: String? = null, +) : SQLiteOpenHelper(context, dbName, null, DATABASE_VERSION) { + companion object { + const val DATABASE_VERSION = 1 + } + + val fullTextSearchModule = FullTextSearchModule() + val eventIndexModule = EventIndexesModule(fullTextSearchModule) + + val replaceableModule = ReplaceableModule() + val addressableModule = AddressableModule() + val ephemeralModule = EphemeralModule() + + val deletionModule = DeletionRequestModule() + val expirationModule = ExpirationModule() + val rightToVanishModule = RightToVanishModule() + + override fun onConfigure(db: SQLiteDatabase) { + super.onConfigure(db) + + // makes sure the FKs are sane + db.setForeignKeyConstraintsEnabled(true) + + // SQLite implements mutations by appending them to a log, which it occasionally + // compacts into the database. This is called Write-Ahead Logging (WAL) + db.enableWriteAheadLogging() + + // The DB can be corrupted if the OS is shutdown before sync, which generally + // doesn't happen on Android + db.execSQL("PRAGMA synchronous = OFF") + } + + override fun onCreate(db: SQLiteDatabase) { + eventIndexModule.create(db) + replaceableModule.create(db) + addressableModule.create(db) + ephemeralModule.create(db) + deletionModule.create(db) + expirationModule.create(db) + rightToVanishModule.create(db) + fullTextSearchModule.create(db) + } + + override fun onUpgrade( + db: SQLiteDatabase, + oldVersion: Int, + newVersion: Int, + ) {} + + fun clearDB() { + val db = writableDatabase + fullTextSearchModule.deleteAll(db) + rightToVanishModule.deleteAll(db) + expirationModule.deleteAll(db) + eventIndexModule.deleteAll(db) + } + + fun insertEvent(event: Event): Boolean { + if (event.isExpired()) throw SQLiteConstraintException("blocked: Cannot insert an expired event") + if (event.kind.isEphemeral()) return false + + val db = writableDatabase + db.transaction { + val headerId = eventIndexModule.insert(event, db) + deletionModule.insert(event, headerId, db) + expirationModule.insert(event, headerId, db) + fullTextSearchModule.insert(event, headerId, db) + rightToVanishModule.insert(event, relayUrl, headerId, db) + } + return true + } + + fun query(filter: Filter): List = eventIndexModule.query(filter, readableDatabase) + + fun query(filters: List): List = eventIndexModule.query(filters, readableDatabase) + + fun query( + filter: Filter, + onEach: (Event) -> Unit, + ) = eventIndexModule.query(filter, readableDatabase, onEach) + + fun query( + filters: List, + onEach: (Event) -> Unit, + ) = eventIndexModule.query(filters, readableDatabase, onEach) + + fun count(filter: Filter): Int = eventIndexModule.count(filter, readableDatabase) + + fun count(filters: List): Int = eventIndexModule.count(filters, readableDatabase) + + fun delete(filter: Filter): Int? = eventIndexModule.delete(filter, writableDatabase) + + fun delete(filters: List): Int? = eventIndexModule.delete(filters, writableDatabase) + + fun delete(id: HexKey): Int = writableDatabase.delete("event_headers", "id = ?", arrayOf(id)) + + fun deleteExpiredEvents() = expirationModule.deleteExpiredEvents(writableDatabase) +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/StatementCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/StatementCache.kt new file mode 100644 index 0000000000..8d30784532 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/StatementCache.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteStatement +import android.util.LruCache + +object StatementCache { + data class StatementKey( + val sql: String, + val dbHashcode: Int, + ) + + val cachedStatements = LruCache(10) + + fun get( + sql: String, + db: SQLiteDatabase, + ): SQLiteStatement { + val key = StatementKey(sql, db.hashCode()) + val cached = cachedStatements.get(key) + return if (cached != null) { + cached.clearBindings() + cached + } else { + val stat = db.compileStatement(sql) + cachedStatements.put(key, stat) + stat + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt new file mode 100644 index 0000000000..c2cd3a6e61 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/Condition.kt @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite.sql + +sealed class Condition { + data class Equals( + val column: String, + val value: Any?, + ) : Condition() + + data class NotEquals( + val column: String, + val value: Any?, + ) : Condition() + + data class GreaterThan( + val column: String, + val value: Any, + ) : Condition() + + data class GreaterThanOrEquals( + val column: String, + val value: Any, + ) : Condition() + + data class LessThan( + val column: String, + val value: Any, + ) : Condition() + + data class LessThanOrEquals( + val column: String, + val value: Any, + ) : Condition() + + data class Like( + val column: String, + val value: String, + ) : Condition() + + data class Match( + val table: String, + val value: String, + ) : Condition() + + data class IsNull( + val column: String, + ) : Condition() + + data class IsNotNull( + val column: String, + ) : Condition() + + data class In( + val column: String, + val values: List, + ) : Condition() + + data class And( + val conditions: List, + ) : Condition() + + data class Or( + val conditions: List, + ) : Condition() +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt new file mode 100644 index 0000000000..7e898c2445 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/SqlSelectionBuilder.kt @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite.sql + +class SqlSelectionBuilder( + private val condition: Condition, +) { + private val selectionArgs = mutableListOf() + + fun build(): WhereClause { + selectionArgs.clear() // Clear previous args for a fresh build + val conditions = buildCondition(condition) + return WhereClause(conditions, selectionArgs) + } + + /** + * Recursively builds the SQL string for a given condition. + * @param cond The [Condition] to build the SQL string for. + * @return The SQL string representation of the condition. + */ + private fun buildCondition(cond: Condition): String = + when (cond) { + is Condition.Equals -> { + selectionArgs.add(cond.value.toString()) + "${cond.column} = ?" + } + is Condition.NotEquals -> { + selectionArgs.add(cond.value.toString()) + "${cond.column} != ?" + } + is Condition.GreaterThan -> { + selectionArgs.add(cond.value.toString()) + "${cond.column} > ?" + } + is Condition.GreaterThanOrEquals -> { + selectionArgs.add(cond.value.toString()) + "${cond.column} >= ?" + } + is Condition.LessThan -> { + selectionArgs.add(cond.value.toString()) + "${cond.column} < ?" + } + is Condition.LessThanOrEquals -> { + selectionArgs.add(cond.value.toString()) + "${cond.column} <= ?" + } + is Condition.Like -> { + selectionArgs.add(cond.value) + "${cond.column} LIKE ?" + } + is Condition.Match -> { + selectionArgs.add(cond.value) + "${cond.table} MATCH ?" + } + is Condition.IsNull -> { + "${cond.column} IS NULL" + } + is Condition.IsNotNull -> { + "${cond.column} IS NOT NULL" + } + is Condition.In -> { + if (cond.values.isEmpty()) { + // Handle empty IN clause gracefully, perhaps by making it always false + // or throwing an error, depending on desired behavior. + // For now, let's make it an always false condition to avoid SQL errors. + "1 = 0" // Always false + } else { + val placeholders = cond.values.joinToString(", ") { "?" } + cond.values.forEach { selectionArgs.add(it.toString()) } + "${cond.column} IN ($placeholders)" + } + } + is Condition.And -> { + if (cond.conditions.isEmpty()) { + "1 = 1" // Always true for an empty AND + } else { + cond.conditions.joinToString(" AND ") { "(${buildCondition(it)})" } + } + } + is Condition.Or -> { + if (cond.conditions.isEmpty()) { + "1 = 0" // Always false for an empty OR + } else { + cond.conditions.joinToString(" OR ") { "(${buildCondition(it)})" } + } + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt new file mode 100644 index 0000000000..817ab0ea23 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/sql/WhereClauseBuilder.kt @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite.sql + +class WhereClauseBuilder { + private val conditions = mutableListOf() + + fun equals( + column: String, + value: Any?, + ) = apply { conditions.add(Condition.Equals(column, value)) } + + fun notEquals( + column: String, + value: Any?, + ) = apply { conditions.add(Condition.NotEquals(column, value)) } + + fun greaterThan( + column: String, + value: Any, + ) = apply { conditions.add(Condition.GreaterThan(column, value)) } + + fun greaterThanOrEquals( + column: String, + value: Any, + ) = apply { conditions.add(Condition.GreaterThanOrEquals(column, value)) } + + fun lessThan( + column: String, + value: Any, + ) = apply { conditions.add(Condition.LessThan(column, value)) } + + fun lessThanOrEquals( + column: String, + value: Any, + ) = apply { conditions.add(Condition.LessThanOrEquals(column, value)) } + + fun like( + column: String, + pattern: String, + ) = apply { conditions.add(Condition.Like(column, pattern)) } + + fun match( + table: String, + pattern: String, + ) = apply { conditions.add(Condition.Match(table, pattern)) } + + fun isNull(column: String) = apply { conditions.add(Condition.IsNull(column)) } + + fun isNotNull(column: String) = apply { conditions.add(Condition.IsNotNull(column)) } + + fun isIn( + column: String, + values: List, + ) = apply { conditions.add(Condition.In(column, values)) } + + fun equalsOrIn( + column: String, + values: List, + ) = apply { + if (values.size == 1) { + equals(column, values.first()) + } else { + isIn(column, values) + } + } + + fun and(block: WhereClauseBuilder.() -> Unit) = + apply { + val builder = WhereClauseBuilder().apply(block) + val builtCondition = builder.build() + if (builtCondition != null) { + conditions.add(builtCondition) + } + } + + fun or(block: WhereClauseBuilder.() -> Unit) = + apply { + val builder = WhereClauseBuilder().apply(block) + val builtCondition = builder.build() + if (builtCondition != null) { + conditions.add(builtCondition) + } + } + + fun build(): Condition? = + when (conditions.size) { + 0 -> null + 1 -> conditions.first() + else -> Condition.And(conditions.toList()) + } +} + +fun where(block: WhereClauseBuilder.() -> Unit): WhereClause { + val condition = WhereClauseBuilder().apply(block).build() ?: Condition.And(emptyList()) + return SqlSelectionBuilder(condition).build() +} + +class WhereClause( + val conditions: String, + val args: List, +) diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventDbQueryAssemblerTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventDbQueryAssemblerTest.kt new file mode 100644 index 0000000000..78e75b1016 --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/store/sqlite/EventDbQueryAssemblerTest.kt @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.store.sqlite + +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import junit.framework.TestCase +import org.junit.Test + +class EventDbQueryAssemblerTest { + val builder = EventIndexesModule(FullTextSearchModule()) + + val key1 = "7c5eb72a4584fdaaeaa145b25c92ea9917704224951219dbd43acef9e91fb88d" + val key2 = "f3ac434d61bc0f491a814782ccfdf9c439dae1f0bde9097ad4a245f4c495cd14" + val key3 = "12ae0fd81c85e1e7d9ed096397dc3129849425fe6f8afce7213ebf38ddfc6ca9" + + @Test + fun testEmpty() { + val sql = builder.planQuery(Filter()) + TestCase.assertEquals( + "SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers ORDER BY created_at DESC, id", + sql, + ) + } + + @Test + fun testLimit() { + val sql = builder.planQuery(Filter(limit = 10)) + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN (SELECT event_headers.row_id as row_id FROM event_headers WHERE 1 = 1 ORDER BY created_at DESC, id ASC LIMIT 10) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + """.trimIndent(), + sql, + ) + } + + @Test + fun testAllFearures() { + val sql = + builder.planQuery( + listOf( + Filter(limit = 10), + Filter(authors = listOf(key1), kinds = listOf(1, 1111), search = "keywords", limit = 100), + Filter(kinds = listOf(20), search = "cats", limit = 30), + ), + ) + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN (SELECT event_headers.row_id as row_id FROM event_headers WHERE 1 = 1 ORDER BY created_at DESC, id ASC LIMIT 10 UNION SELECT event_headers.row_id as row_id FROM event_headers INNER JOIN event_fts ON event_fts.event_header_row_id = event_headers.row_id WHERE (event_headers.kind IN (?, ?)) AND (event_headers.pubkey = ?) AND (event_fts MATCH ?) ORDER BY created_at DESC, id ASC LIMIT 100 UNION SELECT event_headers.row_id as row_id FROM event_headers INNER JOIN event_fts ON event_fts.event_header_row_id = event_headers.row_id WHERE (event_headers.kind = ?) AND (event_fts MATCH ?) ORDER BY created_at DESC, id ASC LIMIT 30) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + """.trimIndent(), + sql, + ) + } + + @Test + fun testIdQuery() { + val sql = builder.planQuery(Filter(ids = listOf(key1))) + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN (SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.id = ?) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + """.trimIndent(), + sql, + ) + } + + @Test + fun testAuthors() { + val sql = builder.planQuery(Filter(authors = listOf(key1, key2))) + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN (SELECT event_headers.row_id as row_id FROM event_headers WHERE event_headers.pubkey IN (?, ?)) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + """.trimIndent(), + sql, + ) + } + + @Test + fun testAuthorsAndSearch() { + val sql = builder.planQuery(Filter(authors = listOf(key1, key2, key3), search = "keywords")) + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN (SELECT event_headers.row_id as row_id FROM event_headers INNER JOIN event_fts ON event_fts.event_header_row_id = event_headers.row_id WHERE (event_headers.pubkey IN (?, ?, ?)) AND (event_fts MATCH ?)) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + """.trimIndent(), + sql, + ) + } + + @Test + fun testKindAndSearch() { + val sql = builder.planQuery(Filter(kinds = listOf(1, 1111, 10000), search = "keywords")) + TestCase.assertEquals( + """ + SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers + INNER JOIN (SELECT event_headers.row_id as row_id FROM event_headers INNER JOIN event_fts ON event_fts.event_header_row_id = event_headers.row_id WHERE (event_headers.kind IN (?, ?, ?)) AND (event_fts MATCH ?)) AS filtered + ON event_headers.row_id = filtered.row_id + ORDER BY created_at DESC, id + """.trimIndent(), + sql, + ) + } +} From bbf6e0260907a7f565f53ed9bda7809498e4a92b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 28 Jul 2025 17:13:57 -0400 Subject: [PATCH 149/185] Beginnings for the migration to DataStore --- amethyst/build.gradle | 1 + .../preferences/AccountPreferenceStores.kt | 82 ++++++++++++++ .../AccountSecretsEncryptedStores.kt | 73 ++++++++++++ .../model/preferences/DataStoreExt.kt | 70 ++++++++++++ .../model/preferences/EncryptedDataStore.kt | 106 ++++++++++++++++++ .../model/preferences/KeyStoreEncryption.kt | 102 +++++++++++++++++ .../preferences/UpdatablePropertyFlow.kt | 43 +++++++ gradle/libs.versions.toml | 2 + 8 files changed, 479 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt diff --git a/amethyst/build.gradle b/amethyst/build.gradle index 0c1e6af165..ab20337e63 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -237,6 +237,7 @@ dependencies { // Encrypted Key Storage implementation libs.androidx.security.crypto.ktx + implementation libs.androidx.datastore.preferences // view videos implementation libs.androidx.media3.exoplayer diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt new file mode 100644 index 0000000000..d847670418 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountPreferenceStores.kt @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.preferences + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import com.vitorpamplona.quartz.utils.LargeCache +import java.io.File + +class AccountPreferenceStores( + val rootFilesDir: () -> File, +) { + companion object { + val defaultHomeFollowList = stringPreferencesKey("defaultHomeFollowList") + val defaultStoriesFollowList = stringPreferencesKey("defaultStoriesFollowList") + val defaultNotificationFollowList = stringPreferencesKey("defaultNotificationFollowList") + val defaultDiscoveryFollowList = stringPreferencesKey("defaultDiscoveryFollowList") + + val localRelayServers = stringPreferencesKey("localRelayServers") + val defaultFileServer = stringPreferencesKey("defaultFileServer") + + val latestUserMetadata = stringPreferencesKey("latestUserMetadata") + val latestContactList = stringPreferencesKey("latestContactList") + val latestDMRelayList = stringPreferencesKey("latestDMRelayList") + val latestNIP65RelayList = stringPreferencesKey("latestNIP65RelayList") + val latestSearchRelayList = stringPreferencesKey("latestSearchRelayList") + val latestBlockedRelayList = stringPreferencesKey("latestBlockedRelayList") + val latestTrustedRelayList = stringPreferencesKey("latestTrustedRelayList") + val latestMuteList = stringPreferencesKey("latestMuteList") + val latestPrivateHomeRelayList = stringPreferencesKey("latestPrivateHomeRelayList") + val latestAppSpecificData = stringPreferencesKey("latestAppSpecificData") + val latestChannelList = stringPreferencesKey("latestChannelList") + val latestCommunityList = stringPreferencesKey("latestCommunityList") + val latestHashtagList = stringPreferencesKey("latestHashtagList") + val latestGeohashList = stringPreferencesKey("latestGeohashList") + val latestEphemeralChatList = stringPreferencesKey("latestEphemeralChatList") + + val hideDeleteRequestDialog = stringPreferencesKey("hideDeleteRequestDialog") + val hideBlockAlertDialog = stringPreferencesKey("hideBlockAlertDialog") + val hideNip17WarningDialog = stringPreferencesKey("hideNip17WarningDialog") + + val torSettings = stringPreferencesKey("tor_settings") + + val hasDonatedInVersion = stringPreferencesKey("hasDonatedInVersion") + } + + private val storeCache = LargeCache>() + + fun file(npub: String) = File(rootFilesDir(), "datastore/$npub.preferences") + + private fun getDataStore(npub: String): DataStore = + storeCache.getOrCreate(npub) { + PreferenceDataStoreFactory.create( + produceFile = { file(npub) }, + ) + } + + fun removeAccount(npub: String) { + file(npub).delete() + storeCache.remove(npub) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt new file mode 100644 index 0000000000..07f561976e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/AccountSecretsEncryptedStores.kt @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.preferences + +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.stringPreferencesKey +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect +import com.vitorpamplona.quartz.utils.LargeCache +import java.io.File + +class AccountSecretsEncryptedStores( + val rootFilesDir: () -> File, +) { + companion object Companion { + val encryption = KeyStoreEncryption() + val key = stringPreferencesKey("privKey") + val nwc = stringPreferencesKey("nwc") + } + + private val storeCache = LargeCache() + + fun file(npub: String) = File(rootFilesDir(), "datastore/$npub.secrets") + + private fun getDataStore(npub: String): EncryptedDataStore = + storeCache.getOrCreate(npub) { + EncryptedDataStore( + PreferenceDataStoreFactory.create( + produceFile = { file(npub) }, + ), + encryption, + ) + } + + suspend fun getPrivateKey(npub: String): String? = getDataStore(npub).get(key) + + suspend fun savePrivateKey( + npub: String, + value: HexKey, + ) { + getDataStore(npub).save(key, value) + } + + suspend fun nwc(npub: String): UpdatablePropertyFlow = + getDataStore(npub).getProperty( + key = nwc, + parser = Nip47WalletConnect.Nip47URI::parser, + serializer = Nip47WalletConnect.Nip47URI::serializer, + ) + + fun removeAccount(npub: String) { + file(npub).delete() + storeCache.remove(npub) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt new file mode 100644 index 0000000000..1e5bc5fbb1 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/DataStoreExt.kt @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.preferences + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope +import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect.Nip47URI.Companion.parser +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.io.IOException + +suspend fun DataStore.getProperty( + key: Preferences.Key, + parser: (String) -> T, + serializer: (T) -> String, +): UpdatablePropertyFlow = + UpdatablePropertyFlow( + flow = + data + .catch { e -> + if (e is IOException) emit(emptyPreferences()) else throw e + }.map { prefs -> + val value = prefs[key] + if (value != null) { + parser(value) + } else { + null + } + }, + update = { newValue -> + if (newValue != null) { + val serialized = serializer(newValue) + if (serialized.isNotBlank()) { + edit { prefs -> + prefs[key] = serialized + } + } else { + edit { prefs -> + prefs.remove(key) + } + } + } else { + edit { prefs -> + prefs.remove(key) + } + } + }, + scope = scope, + ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt new file mode 100644 index 0000000000..b14d65f288 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/EncryptedDataStore.kt @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.preferences + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.scope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import java.io.IOException +import java.util.Base64 + +class EncryptedDataStore( + private val store: DataStore, + private val encryption: KeyStoreEncryption = KeyStoreEncryption(), +) { + private fun decode(str: String): ByteArray = Base64.getDecoder().decode(str) + + private fun encode(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes) + + private fun encrypt(value: String): String = encode(encryption.encrypt(value.toByteArray())) + + private fun decrypt(value: String): String = encryption.decrypt(decode(value)).contentToString() + + suspend fun remove(key: Preferences.Key) { + store.edit { prefs -> + prefs.remove(key) + } + } + + suspend fun save( + key: Preferences.Key, + value: String, + ) { + store.edit { prefs -> + prefs[key] = encrypt(value) + } + } + + suspend fun get(key: Preferences.Key): String? = + store.data + .catch { e -> + if (e is IOException) emit(emptyPreferences()) else throw e + }.firstOrNull() + ?.get(key) + ?.let { decrypt(it) } + + suspend fun getProperty( + key: Preferences.Key, + parser: (String) -> T, + serializer: (T) -> String, + ): UpdatablePropertyFlow = + UpdatablePropertyFlow( + flow = + store.data + .catch { e -> + if (e is IOException) emit(emptyPreferences()) else throw e + }.map { prefs -> + val value = prefs[key] + if (value != null) { + val decrypted = decrypt(value) + if (decrypted.isNotBlank()) { + parser(decrypted) + } else { + null + } + } else { + null + } + }, + update = { newValue -> + if (newValue != null) { + val serialized = serializer(newValue) + if (serialized.isNotBlank()) { + save(key, serialized) + } else { + remove(key) + } + } else { + remove(key) + } + }, + scope = scope, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt new file mode 100644 index 0000000000..eca8de6873 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/KeyStoreEncryption.kt @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.preferences + +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.security.keystore.StrongBoxUnavailableException +import java.security.KeyStore +import javax.crypto.Cipher +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.spec.IvParameterSpec + +class KeyStoreEncryption { + companion object { + private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES + private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM + private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 + private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING" + private const val PURPOSE = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + private const val KEY_ALIAS = "AMETHYST_AES_KEY" + } + + private val cipher = Cipher.getInstance(TRANSFORMATION) + private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } + + private fun getKey(): SecretKey { + val existingKey = keyStore.getEntry(KEY_ALIAS, null) as? KeyStore.SecretKeyEntry + return existingKey?.secretKey ?: createKey() + } + + private fun createKeyStrongBoxIfAvailable(): SecretKey? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + val keyParams = + KeyGenParameterSpec + .Builder(KEY_ALIAS, PURPOSE) + .setBlockModes(BLOCK_MODE) + .setEncryptionPaddings(PADDING) + .setIsStrongBoxBacked(true) + .build() + + val generator = KeyGenerator.getInstance(ALGORITHM) + generator.init(keyParams) + generator.generateKey() + } catch (_: StrongBoxUnavailableException) { + null + } + } else { + null + } + + private fun createKeyRegular(): SecretKey { + val keyParams = + KeyGenParameterSpec + .Builder(KEY_ALIAS, PURPOSE) + .setBlockModes(BLOCK_MODE) + .setEncryptionPaddings(PADDING) + .build() + + val generator = KeyGenerator.getInstance(ALGORITHM) + generator.init(keyParams) + return generator.generateKey() + } + + private fun createKey(): SecretKey = createKeyStrongBoxIfAvailable() ?: createKeyRegular() + + fun encrypt(bytes: ByteArray): ByteArray { + // Initializes the cipher in encrypt mode and encrypts data + cipher.init(Cipher.ENCRYPT_MODE, getKey()) + val iv = cipher.iv + val encrypted = cipher.doFinal(bytes) + return iv + encrypted + } + + fun decrypt(bytes: ByteArray): ByteArray? { + // Extracts IV and decrypts the data + val iv = bytes.copyOfRange(0, cipher.blockSize) + val data = bytes.copyOfRange(cipher.blockSize, bytes.size) + cipher.init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv)) + return cipher.doFinal(data) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt new file mode 100644 index 0000000000..af96b24f56 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/UpdatablePropertyFlow.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.preferences + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.stateIn + +class UpdatablePropertyFlow( + flow: Flow, + val update: suspend (T?) -> Unit, + val scope: CoroutineScope, +) { + val stateFlow = + flow + .flowOn(Dispatchers.Default) + .stateIn( + scope = scope, + started = SharingStarted.Eagerly, + initialValue = null, + ) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 604fa82394..b8d3de626b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ biometricKtx = "1.2.0-alpha05" coil = "3.3.0" composeBom = "2025.07.00" coreKtx = "1.16.0" +datastore = "1.1.7" espressoCore = "3.6.1" firebaseBom = "34.0.0" fragmentKtx = "1.8.8" @@ -69,6 +70,7 @@ androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = " androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidxCamera" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } From 4cc5aaa49dd7739ba3ede77f7d5132462ff27640 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 29 Jul 2025 15:02:16 -0400 Subject: [PATCH 150/185] Adds view support for YakBak --- .../amethyst/model/LocalCache.kt | 16 +++ .../playback/composable/RenderVideoPlayer.kt | 3 +- .../service/playback/composable/VideoView.kt | 11 +- .../playback/composable/VideoViewInner.kt | 3 +- .../playback/composable/wavefront/Waveform.kt | 6 +- .../amethyst/ui/note/types/VoiceTrack.kt | 105 ++++++++++++++++++ .../home/dal/HomeConversationsFeedFilter.kt | 2 + .../home/dal/HomeNewThreadFeedFilter.kt | 2 + .../nip01Core/FilterHomePostsByGeohashes.kt | 4 + .../nip01Core/FilterHomePostsByHashtags.kt | 4 + .../nip65Follows/FilterHomePostsByAuthors.kt | 4 + .../loggedIn/threadview/ThreadFeedView.kt | 3 + .../com/vitorpamplona/quartz/EventFactory.kt | 4 + .../quartz/nipA0VoiceMessages/AudioMeta.kt | 51 +++++++++ .../nipA0VoiceMessages/BaseVoiceEvent.kt | 56 ++++++++++ .../nipA0VoiceMessages/IMetaTagBuilderExt.kt | 36 ++++++ .../quartz/nipA0VoiceMessages/IMetaTagExt.kt | 32 ++++++ .../nipA0VoiceMessages/TagArrayBuilderExt.kt | 42 +++++++ .../quartz/nipA0VoiceMessages/VoiceEvent.kt | 47 ++++++++ .../nipA0VoiceMessages/VoiceReplyEvent.kt | 47 ++++++++ .../nipA0VoiceMessages/tags/DurationTag.kt | 41 +++++++ .../nipA0VoiceMessages/tags/HashSha256Tag.kt | 41 +++++++ .../nipA0VoiceMessages/tags/WaveformTag.kt | 60 ++++++++++ 23 files changed, 610 insertions(+), 10 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/AudioMeta.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/BaseVoiceEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceReplyEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/DurationTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/HashSha256Tag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/WaveformTag.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 47580cabf9..c5c576b4a1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -179,6 +179,8 @@ import com.vitorpamplona.quartz.nip90Dvms.NIP90UserDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.LargeCache @@ -599,6 +601,18 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) + fun consume( + event: VoiceEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeRegularEvent(event, relay, wasVerified) + + fun consume( + event: VoiceReplyEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ) = consumeRegularEvent(event, relay, wasVerified) + @Suppress("DEPRECATION") fun consume( event: TorrentCommentEvent, @@ -2816,6 +2830,8 @@ object LocalCache : ILocalCache { is TrustedRelayListEvent -> consume(event, relay, wasVerified) is VideoHorizontalEvent -> consume(event, relay, wasVerified) is VideoVerticalEvent -> consume(event, relay, wasVerified) + is VoiceEvent -> consume(event, relay, wasVerified) + is VoiceReplyEvent -> consume(event, relay, wasVerified) is WikiNoteEvent -> consume(event, relay, wasVerified) else -> { Log.w("Event Not Supported", event.toJson()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt index 458ad57f42..1a9e667062 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/RenderVideoPlayer.kt @@ -40,7 +40,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.RenderCon import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.experimental.audio.header.tags.WaveformTag @Composable @OptIn(UnstableApi::class) @@ -50,7 +49,7 @@ fun RenderVideoPlayer( thumbData: VideoThumb?, showControls: Boolean = true, contentScale: ContentScale, - waveform: WaveformTag? = null, + waveform: WaveformData? = null, borderModifier: Modifier, videoModifier: Modifier, onControllerVisibilityChanged: ((Boolean) -> Unit)? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt index 011d825048..8b31f98922 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoView.kt @@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -42,9 +43,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.videoGalleryModifier -import com.vitorpamplona.quartz.experimental.audio.header.tags.WaveformTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +@Immutable +class WaveformData( + val wave: List, +) + @Composable fun VideoView( videoUri: String, @@ -54,7 +59,7 @@ fun VideoView( roundedCorner: Boolean, gallery: Boolean = false, contentScale: ContentScale, - waveform: WaveformTag? = null, + waveform: WaveformData? = null, artworkUri: String? = null, authorName: String? = null, dimensions: DimensionTag? = null, @@ -85,7 +90,7 @@ fun VideoView( thumb: VideoThumb? = null, borderModifier: Modifier, contentScale: ContentScale, - waveform: WaveformTag? = null, + waveform: WaveformData? = null, artworkUri: String? = null, authorName: String? = null, dimensions: DimensionTag? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt index 61eac519f2..03aca3d3d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/VideoViewInner.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.layout.ContentScale import com.vitorpamplona.amethyst.service.playback.composable.mainVideo.VideoPlayerActiveMutex import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.experimental.audio.header.tags.WaveformTag public val DEFAULT_MUTED_SETTING = mutableStateOf(true) @@ -43,7 +42,7 @@ fun VideoViewInner( showControls: Boolean = true, contentScale: ContentScale, borderModifier: Modifier, - waveform: WaveformTag? = null, + waveform: WaveformData? = null, artworkUri: String? = null, authorName: String? = null, nostrUriCallback: String? = null, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt index 3307cbf99d..c0ef0c8659 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt @@ -38,15 +38,15 @@ import androidx.media3.common.Player import androidx.media3.session.MediaController import com.linc.audiowaveform.infiniteLinearGradient import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.ui.components.AudioWaveformReadOnly -import com.vitorpamplona.quartz.experimental.audio.header.tags.WaveformTag import kotlinx.coroutines.delay import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flow @Composable fun Waveform( - waveform: WaveformTag, + waveform: WaveformData, mediaControllerState: MediaControllerState, modifier: Modifier, ) { @@ -94,7 +94,7 @@ private fun pollCurrentDuration(controller: MediaController) = @Composable fun DrawWaveform( - waveform: WaveformTag, + waveform: WaveformData, waveformProgress: MutableFloatState, modifier: Modifier, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt new file mode 100644 index 0000000000..5233140938 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.types + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.playback.composable.VideoView +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags +import com.vitorpamplona.quartz.nip14Subject.subject +import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent + +@Composable +fun RenderVoiceTrack( + note: Note, + contentScale: ContentScale, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? BaseVoiceEvent ?: return + + AudioVoiceHeader(noteEvent, note, contentScale, accountViewModel, nav) +} + +@Composable +fun AudioVoiceHeader( + noteEvent: BaseVoiceEvent, + note: Note, + contentScale: ContentScale, + accountViewModel: AccountViewModel, + nav: INav, +) { + val waveform = + remember(noteEvent) { + noteEvent + .iMetaTags() + .firstOrNull() + ?.waveform + ?.let { WaveformData(it) } + } + val media = + remember(noteEvent) { + noteEvent.content.ifBlank { null } ?: noteEvent.iMetaTags().firstOrNull()?.url + } + + if (media == null) return + + Column(modifier = Modifier.fillMaxWidth().padding(top = 5.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + VideoView( + videoUri = media, + mimeType = null, + waveform = waveform, + title = noteEvent.subject(), + authorName = note.author?.toBestDisplayName(), + roundedCorner = true, + contentScale = contentScale, + accountViewModel = accountViewModel, + nostrUriCallback = note.toNostrUri(), + ) + } + + val callbackUri = remember(note) { note.toNostrUri() } + + if (noteEvent.hasHashtags()) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + DisplayUncitedHashtags(noteEvent, callbackUri, accountViewModel, nav) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt index 5c940a8fa8..0ff7d6fab8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeConversationsFeedFilter.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent class HomeConversationsFeedFilter( val account: Account, @@ -79,6 +80,7 @@ class HomeConversationsFeedFilter( event is PollNoteEvent || event is ChannelMessageEvent || event is CommentEvent || + event is VoiceReplyEvent || event is LiveActivitiesChatMessageEvent ) && filterParams.match(event) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index e5a0441fe7..806516e96d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent class HomeNewThreadFeedFilter( val account: Account, @@ -100,6 +101,7 @@ class HomeNewThreadFeedFilter( noteEvent is InteractiveStoryPrologueEvent || noteEvent is CommentEvent || noteEvent is AudioTrackEvent || + noteEvent is VoiceEvent || noteEvent is AudioHeaderEvent ) && filterParams.match(noteEvent, it.relays) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt index 95a28c771d..a5e07906d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByGeohashes.kt @@ -35,6 +35,8 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import kotlin.collections.flatten val HomePostsByGeohashKinds = @@ -48,6 +50,8 @@ val HomePostsByGeohashKinds = InteractiveStoryPrologueEvent.KIND, WikiNoteEvent.KIND, CommentEvent.KIND, + VoiceEvent.KIND, + VoiceReplyEvent.KIND, ) fun filterHomePostsByGeohashes( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt index b94679fb48..8e9eddd2be 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt @@ -37,6 +37,8 @@ import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent val HomePostsBuHashtagsKinds = listOf( @@ -50,6 +52,8 @@ val HomePostsBuHashtagsKinds = InteractiveStoryPrologueEvent.KIND, CommentEvent.KIND, WikiNoteEvent.KIND, + VoiceEvent.KIND, + VoiceReplyEvent.KIND, ) fun filterHomePostsByHashtags( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt index 80c915eadb..b9de073882 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -40,6 +40,8 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEven import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import kotlin.math.max val HomePostsNewThreadKinds = @@ -61,6 +63,8 @@ val HomePostsConversationKinds = CommentEvent.KIND, LiveActivitiesEvent.KIND, EphemeralChatEvent.KIND, + VoiceEvent.KIND, + VoiceReplyEvent.KIND, ) fun filterNewHomePostsByAuthors( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index ae82881ca8..63861a7646 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -229,6 +229,7 @@ import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking @@ -547,6 +548,8 @@ private fun FullBleedNoteCompose( VideoDisplay(baseNote, makeItShort = false, canPreview = true, backgroundColor = backgroundColor, ContentScale.FillWidth, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is PictureEvent) { PictureDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, PaddingValues(vertical = Size5dp), backgroundColor, accountViewModel = accountViewModel, nav) + } else if (noteEvent is BaseVoiceEvent) { + VoiceEventDisplay(baseNote, makeItShort = false, canPreview = true, backgroundColor = backgroundColor, ContentScale.FillWidth, accountViewModel = accountViewModel, nav = nav) } else if (noteEvent is FileHeaderEvent) { FileHeaderDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 2a0fa369e6..82a941199c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -125,6 +125,8 @@ import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import com.vitorpamplona.quartz.nip96FileStorage.config.FileServersEvent import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomAuthorizationEvent import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent @@ -282,6 +284,8 @@ class EventFactory { TrustedRelayListEvent.KIND -> TrustedRelayListEvent(id, pubKey, createdAt, tags, content, sig) VideoHorizontalEvent.KIND -> VideoHorizontalEvent(id, pubKey, createdAt, tags, content, sig) VideoVerticalEvent.KIND -> VideoVerticalEvent(id, pubKey, createdAt, tags, content, sig) + VoiceEvent.KIND -> VoiceEvent(id, pubKey, createdAt, tags, content, sig) + VoiceReplyEvent.KIND -> VoiceReplyEvent(id, pubKey, createdAt, tags, content, sig) WikiNoteEvent.KIND -> WikiNoteEvent(id, pubKey, createdAt, tags, content, sig) else -> { factories[kind]?.build(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/AudioMeta.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/AudioMeta.kt new file mode 100644 index 0000000000..c74d41ff63 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/AudioMeta.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages + +import com.vitorpamplona.quartz.nip92IMeta.IMetaTag +import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.WaveformTag + +data class AudioMeta( + val url: String, + val hash: String? = null, + val duration: Int? = null, + val waveform: List? = null, +) { + fun toIMetaArray(): Array = + IMetaTagBuilder(url) + .apply { + hash?.let { hash(it) } + duration?.let { duration(it) } + waveform?.let { waveform(it) } + }.build() + .toTagArray() + + companion object { + fun parse(iMeta: IMetaTag): AudioMeta = + AudioMeta( + iMeta.url, + iMeta.hash()?.firstOrNull(), + iMeta.duration()?.firstOrNull()?.toIntOrNull(), + iMeta.waveform()?.firstOrNull()?.let { WaveformTag.parseWave(it) }, + ) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/BaseVoiceEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/BaseVoiceEvent.kt new file mode 100644 index 0000000000..6fb98c5b9d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/BaseVoiceEvent.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip92IMeta.imetas +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +open class BaseVoiceEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun iMetaTags() = imetas().map { AudioMeta.parse(it) } + + companion object { + fun build( + voiceMessage: AudioMeta, + kind: Int, + alt: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(kind, voiceMessage.url, createdAt) { + alt(alt) + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagBuilderExt.kt new file mode 100644 index 0000000000..226d110ecc --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagBuilderExt.kt @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.DurationTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.HashSha256Tag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.WaveformTag + +/** + * Contains the IMeta tags that are used by Picture events. + */ +fun IMetaTagBuilder.hash(hash: HexKey) = add(HashSha256Tag.TAG_NAME, hash) + +fun IMetaTagBuilder.duration(size: Int) = add(DurationTag.TAG_NAME, size.toString()) + +fun IMetaTagBuilder.waveform(wave: List) = add(WaveformTag.TAG_NAME, WaveformTag.assembleWave(wave)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagExt.kt new file mode 100644 index 0000000000..d65ab34e96 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagExt.kt @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages + +import com.vitorpamplona.quartz.nip92IMeta.IMetaTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.DurationTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.HashSha256Tag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.WaveformTag + +fun IMetaTag.hash() = properties.get(HashSha256Tag.TAG_NAME) + +fun IMetaTag.duration() = properties.get(DurationTag.TAG_NAME) + +fun IMetaTag.waveform() = properties.get(WaveformTag.TAG_NAME) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..1854a71bf1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/TagArrayBuilderExt.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashSha256Tag + +fun TagArrayBuilder.audioIMeta( + url: String, + hash: String? = null, + duration: Int? = null, + waveform: List? = null, +) = audioIMeta(AudioMeta(url, hash, duration, waveform)) + +fun TagArrayBuilder.audioIMeta(imeta: AudioMeta): TagArrayBuilder { + add(imeta.toIMetaArray()) + imeta.hash?.let { add(HashSha256Tag.assemble(it)) } + return this +} + +fun TagArrayBuilder.audioIMeta(audioUrls: List): TagArrayBuilder { + audioUrls.forEach { audioIMeta(it) } + return this +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceEvent.kt new file mode 100644 index 0000000000..a43185712b --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceEvent.kt @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class VoiceEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseVoiceEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = 1222 + const val ALT_DESCRIPTION = "Voice message" + + fun build( + voiceMessage: AudioMeta, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(voiceMessage, KIND, ALT_DESCRIPTION, createdAt, initializer) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceReplyEvent.kt new file mode 100644 index 0000000000..5d43098cd2 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceReplyEvent.kt @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class VoiceReplyEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseVoiceEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = 1244 + const val ALT_DESCRIPTION = "Voice reply" + + fun build( + voiceMessage: AudioMeta, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = build(voiceMessage, KIND, ALT_DESCRIPTION, createdAt, initializer) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/DurationTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/DurationTag.kt new file mode 100644 index 0000000000..af56d81f2d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/DurationTag.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +class DurationTag { + companion object { + const val TAG_NAME = "duration" + + @JvmStatic + fun parse(tag: Array): Int? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1].toIntOrNull() + } + + @JvmStatic + fun assemble(seconds: Int) = arrayOf(TAG_NAME, seconds.toString()) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/HashSha256Tag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/HashSha256Tag.kt new file mode 100644 index 0000000000..9f42b8ad7f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/HashSha256Tag.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +class HashSha256Tag { + companion object { + const val TAG_NAME = "sha256" + + @JvmStatic + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + @JvmStatic + fun assemble(hash: String) = arrayOf(TAG_NAME, hash) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/WaveformTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/WaveformTag.kt new file mode 100644 index 0000000000..d4366fcba5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/WaveformTag.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +class WaveformTag( + val wave: List, +) { + fun toTagArray() = assemble(wave) + + companion object { + const val TAG_NAME = "waveform" + + @JvmStatic + fun parse(tag: Array): WaveformTag? = parseWave(tag)?.let { WaveformTag(it) } + + @JvmStatic + fun parseWave(tag: Array): List? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + + val wave = tag[1].split(" ").mapNotNull { it.toIntOrNull() } + if (wave.isEmpty()) return null + return wave + } + + fun parseWave(wave: String): List? { + val wave = wave.split(" ").mapNotNull { it.toIntOrNull() } + if (wave.isEmpty()) return null + return wave + } + + @JvmStatic + fun assembleWave(wave: List) = wave.joinToString(" ") + + @JvmStatic + fun assemble(wave: List) = arrayOf(TAG_NAME, assembleWave(wave)) + } +} From 35a0b04020f6cafda640d1012ae1b82e0fe8359a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 29 Jul 2025 15:03:44 -0400 Subject: [PATCH 151/185] Adds view support for public messages --- .../amethyst/model/LocalCache.kt | 12 +- .../FilterNotificationsToPubkey.kt | 2 + .../subassemblies/SearchPostsByText.kt | 3 +- .../amethyst/ui/note/NoteCompose.kt | 17 ++- .../amethyst/ui/note/types/PublicMessage.kt | 88 +++++++++++++++ .../loggedIn/home/ShortNotePostScreen.kt | 5 +- .../loggedIn/home/ShortNotePostViewModel.kt | 52 ++++----- .../home/dal/HomeNewThreadFeedFilter.kt | 2 + .../loggedIn/threadview/ThreadFeedView.kt | 4 + amethyst/src/main/res/values/strings.xml | 1 + .../com/vitorpamplona/quartz/EventFactory.kt | 2 + .../publicMessages/PublicMessageEvent.kt | 96 ++++++++++++++++ .../publicMessages/TagArrayBuilderExt.kt | 28 +++++ .../publicMessages/tags/ReceiverTag.kt | 95 ++++++++++++++++ .../quartz/nip10Notes/BaseNoteEvent.kt | 103 ++++++++++++++++++ .../quartz/nip10Notes/BaseThreadedEvent.kt | 70 +----------- 16 files changed, 472 insertions(+), 108 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/PublicMessageEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/TagArrayBuilderExt.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/tags/ReceiverTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseNoteEvent.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index c5c576b4a1..8409dd9072 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.checkSignature @@ -84,7 +85,7 @@ import com.vitorpamplona.quartz.nip03Timestamp.VerificationState import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent import com.vitorpamplona.quartz.nip09Deletions.DeletionIndex -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent @@ -535,6 +536,12 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) + fun consume( + event: PublicMessageEvent, + relay: NormalizedRelayUrl? = null, + wasVerified: Boolean, + ) = consumeRegularEvent(event, relay, wasVerified) + fun consume( event: TorrentEvent, relay: NormalizedRelayUrl?, @@ -575,7 +582,7 @@ object LocalCache : ILocalCache { // Already processed this event. if (note.event != null) return false - if (event is BaseThreadedEvent && antiSpam.isSpam(event, relay)) { + if (event is BaseNoteEvent && antiSpam.isSpam(event, relay)) { return false } @@ -2813,6 +2820,7 @@ object LocalCache : ILocalCache { is PrivateOutboxRelayListEvent -> consume(event, relay, wasVerified) is ProxyRelayListEvent -> consume(event, relay, wasVerified) is PinListEvent -> consume(event, relay, wasVerified) + is PublicMessageEvent -> consume(event, relay, wasVerified) is PeopleListEvent -> consume(event, relay, wasVerified) is PollNoteEvent -> consume(event, relay, wasVerified) is ReactionEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt index 60b3377649..6da7409fcf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.nip01N import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -63,6 +64,7 @@ val NotificationsPerKeyKinds = EphemeralChatEvent.KIND, BadgeAwardEvent.KIND, PollNoteEvent.KIND, + PublicMessageEvent.KIND, ) val NotificationsPerKeyKinds2 = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt index 21b3ce2ff9..dd1cf3e629 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -69,7 +70,7 @@ val SearchPostsByTextKinds2 = EmojiPackEvent.KIND, HighlightEvent.KIND, LiveActivitiesEvent.KIND, - PollNoteEvent.KIND, + PublicMessageEvent.KIND, NNSEvent.KIND, WikiNoteEvent.KIND, CommentEvent.KIND, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 71898328a3..7a887efe33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -119,12 +119,14 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent import com.vitorpamplona.amethyst.ui.note.types.RenderPoll import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval import com.vitorpamplona.amethyst.ui.note.types.RenderPrivateMessage +import com.vitorpamplona.amethyst.ui.note.types.RenderPublicMessage import com.vitorpamplona.amethyst.ui.note.types.RenderReaction import com.vitorpamplona.amethyst.ui.note.types.RenderReport import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent import com.vitorpamplona.amethyst.ui.note.types.RenderTorrent import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment +import com.vitorpamplona.amethyst.ui.note.types.RenderVoiceTrack import com.vitorpamplona.amethyst.ui.note.types.RenderWikiContent import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -160,6 +162,7 @@ import com.vitorpamplona.quartz.experimental.forks.isAFork import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableKind import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope @@ -214,6 +217,7 @@ import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90StatusEvent import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import kotlinx.coroutines.delay @Composable @@ -790,7 +794,7 @@ private fun RenderNoteRow( is VideoHorizontalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav) is VideoVerticalEvent -> VideoDisplay(baseNote, makeItShort, canPreview, backgroundColor, ContentScale.FillWidth, accountViewModel, nav) is PictureEvent -> PictureDisplay(baseNote, true, ContentScale.FillWidth, PaddingValues(vertical = 5.dp), backgroundColor, accountViewModel, nav) - + is BaseVoiceEvent -> RenderVoiceTrack(baseNote, ContentScale.FillWidth, accountViewModel, nav) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, true, ContentScale.FillWidth, accountViewModel) is CommunityPostApprovalEvent -> { RenderPostApproval( @@ -865,7 +869,16 @@ private fun RenderNoteRow( accountViewModel, nav, ) - + is PublicMessageEvent -> + RenderPublicMessage( + baseNote, + makeItShort, + canPreview, + quotesLeft, + backgroundColor, + accountViewModel, + nav, + ) else -> { RenderTextEvent( baseNote, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt new file mode 100644 index 0000000000..69dd2052cd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/PublicMessage.kt @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.types + +import 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.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.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags +import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists + +@Composable +fun RenderPublicMessage( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? PublicMessageEvent ?: return + + if (makeItShort && accountViewModel.isLoggedUser(note.author)) { + Text( + text = noteEvent.content, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + val callbackUri = remember(note) { note.toNostrUri() } + + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + TranslatableRichTextViewer( + content = noteEvent.content, + canPreview = canPreview && !makeItShort, + quotesLeft = quotesLeft, + modifier = Modifier.fillMaxWidth(), + tags = remember(noteEvent) { noteEvent.tags.toImmutableListOfLists() }, + backgroundColor = backgroundColor, + id = note.idHex, + callbackUri = callbackUri, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (noteEvent.hasHashtags()) { + DisplayUncitedHashtags(noteEvent, noteEvent.content, callbackUri, accountViewModel, nav) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index eb67689f16..ec309dcfd4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -52,7 +52,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import androidx.core.util.Consumer -import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note @@ -173,7 +172,7 @@ private fun NewPostScreenInner( onPost = { // uses the accountViewModel scope to avoid cancelling this // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { postViewModel.sendPostSync() delay(100) nav.popBack() @@ -182,7 +181,7 @@ private fun NewPostScreenInner( onCancel = { // uses the accountViewModel scope to avoid cancelling this // function when the postViewModel is released - accountViewModel.viewModelScope.launch(Dispatchers.IO) { + accountViewModel.runIOCatching { postViewModel.sendDraftSync() nav.popBack() postViewModel.cancel() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 49cb45da9c..30d88edf54 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -136,6 +136,9 @@ open class ShortNotePostViewModel : IZapRaiser { val draftTag = DraftTagState() + lateinit var accountViewModel: AccountViewModel + lateinit var account: Account + init { viewModelScope.launch(Dispatchers.IO) { draftTag.versions.collectLatest { @@ -147,9 +150,6 @@ open class ShortNotePostViewModel : } } - var accountViewModel: AccountViewModel? = null - var account: Account? = null - var originalNote: Note? by mutableStateOf(null) var forkedFromNote: Note? by mutableStateOf(null) @@ -157,8 +157,7 @@ open class ShortNotePostViewModel : var eTags by mutableStateOf?>(null) val iMetaAttachments = IMetaAttachments() - var nip95attachments by - mutableStateOf>>(emptyList()) + var nip95attachments by mutableStateOf>>(emptyList()) override var message by mutableStateOf(TextFieldValue("")) @@ -213,11 +212,11 @@ open class ShortNotePostViewModel : var wantsZapRaiser by mutableStateOf(false) override val zapRaiserAmount = mutableStateOf(null) - fun lnAddress(): String? = account?.userProfile()?.info?.lnAddress() + fun lnAddress(): String? = account.userProfile().info?.lnAddress() - fun hasLnAddress(): Boolean = account?.userProfile()?.info?.lnAddress() != null + fun hasLnAddress(): Boolean = account.userProfile().info?.lnAddress() != null - fun user(): User? = account?.userProfile() + fun user(): User? = account.userProfile() open fun init(accountVM: AccountViewModel) { this.accountViewModel = accountVM @@ -239,7 +238,6 @@ open class ShortNotePostViewModel : version: Note?, draft: Note?, ) { - val accountViewModel = accountViewModel ?: return val noteEvent = draft?.event val noteAuthor = draft?.author @@ -369,8 +367,8 @@ open class ShortNotePostViewModel : } private fun loadFromDraft(draftEvent: TextNoteEvent) { - canAddInvoice = accountViewModel?.userProfile()?.info?.lnAddress() != null - canAddZapRaiser = accountViewModel?.userProfile()?.info?.lnAddress() != null + canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null + canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null multiOrchestrator = null val localForwardZapTo = draftEvent.tags.filter { it.size > 1 && it[0] == "zap" } @@ -475,15 +473,15 @@ open class ShortNotePostViewModel : } } - accountViewModel?.account?.signAndComputeBroadcast(template, extraNotesToBroadcast) + accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) - accountViewModel?.deleteDraft(draftTag.current) + accountViewModel.deleteDraft(draftTag.current) cancel() } suspend fun sendDraftSync() { - val accountViewModel = accountViewModel ?: return + val accountViewModel = accountViewModel if (message.text.isBlank()) { accountViewModel.account.deleteDraft(draftTag.current) @@ -492,24 +490,19 @@ open class ShortNotePostViewModel : accountViewModel.account.createAndSendDraft(draftTag.current, template) nip95attachments.forEach { - account?.sendToPrivateOutboxAndLocal(it.first) - account?.sendToPrivateOutboxAndLocal(it.second) + account.sendToPrivateOutboxAndLocal(it.first) + account.sendToPrivateOutboxAndLocal(it.second) } } } private suspend fun createTemplate(): EventTemplate? { - if (accountViewModel == null) { - cancel() - return null - } - val tagger = NewMessageTagger( message.text, pTags, eTags, - accountViewModel!!, + accountViewModel, ) tagger.run() @@ -518,7 +511,7 @@ open class ShortNotePostViewModel : val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() val localZapRaiserAmount = if (wantsZapRaiser) zapRaiserAmount.value else null - val emojis = findEmoji(tagger.message, account?.emoji?.myEmojis?.value) + val emojis = findEmoji(tagger.message, account.emoji.myEmojis.value) val urls = findURLs(tagger.message) val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet()) @@ -607,8 +600,6 @@ open class ShortNotePostViewModel : context: Context, ) { viewModelScope.launch(Dispatchers.Default) { - val myAccount = account ?: return@launch - val myMultiOrchestrator = multiOrchestrator ?: return@launch isUploadingImage = true @@ -619,16 +610,16 @@ open class ShortNotePostViewModel : contentWarningReason, MediaCompressor.intToCompressorQuality(mediaQuality), server, - myAccount, + account, context, ) if (results.allGood) { results.successful.forEach { state -> if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { - val nip95 = myAccount.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) + val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) nip95attachments = nip95attachments + nip95 - val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } + val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.let { message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) @@ -786,8 +777,7 @@ open class ShortNotePostViewModel : item.link.url, ) { Amethyst.instance.okHttpClients.getHttpClient( - accountViewModel?.account?.privacyState?.shouldUseTorForImageDownload(item.link.url) - ?: false, + accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url), ) } } @@ -870,7 +860,7 @@ open class ShortNotePostViewModel : override fun updateZapFromText() { viewModelScope.launch(Dispatchers.Default) { val tagger = - NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!) + NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel) tagger.run() tagger.pTags?.forEach { taggedUser -> if (!forwardZapTo.value.items.any { it.key == taggedUser }) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt index 806516e96d..a92e458716 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeNewThreadFeedFilter.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent @@ -98,6 +99,7 @@ class HomeNewThreadFeedFilter( (noteEvent is WikiNoteEvent && noteEvent.content.isNotEmpty()) || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || + (noteEvent is PublicMessageEvent && noteEvent.content.isNotEmpty() && noteEvent.isIncluded(account.signer.pubKey)) || noteEvent is InteractiveStoryPrologueEvent || noteEvent is CommentEvent || noteEvent is AudioTrackEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 63861a7646..06841d4214 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -148,6 +148,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent import com.vitorpamplona.amethyst.ui.note.types.RenderPoll import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval import com.vitorpamplona.amethyst.ui.note.types.RenderPrivateMessage +import com.vitorpamplona.amethyst.ui.note.types.RenderPublicMessage import com.vitorpamplona.amethyst.ui.note.types.RenderTextEvent import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent import com.vitorpamplona.amethyst.ui.note.types.RenderTorrent @@ -184,6 +185,7 @@ import com.vitorpamplona.quartz.experimental.forks.forkFromAddress import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.addressables.isTaggedAddressableKind @@ -642,6 +644,8 @@ private fun FullBleedNoteCompose( RenderDraft(baseNote, 3, true, backgroundColor, accountViewModel, nav) } else if (noteEvent is HighlightEvent) { RenderHighlight(baseNote, false, canPreview, quotesLeft = 3, backgroundColor, accountViewModel, nav) + } else if (noteEvent is PublicMessageEvent) { + RenderPublicMessage(baseNote, false, canPreview, quotesLeft = 3, backgroundColor, accountViewModel, nav) } else if (noteEvent is CommentEvent) { RenderTextEvent( baseNote, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 40d6c2bf4f..3ccb82c9b5 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1214,6 +1214,7 @@ Log off on device lock Private Message + Public Message Chat Relay The relay that all users of this chat connect to diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt index 82a941199c..4477dfe7d5 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/EventFactory.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.relationshipStatus.RelationshipStatusEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -267,6 +268,7 @@ class EventFactory { PrivateDmEvent.KIND -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig) PrivateOutboxRelayListEvent.KIND -> PrivateOutboxRelayListEvent(id, pubKey, createdAt, tags, content, sig) ProxyRelayListEvent.KIND -> ProxyRelayListEvent(id, pubKey, createdAt, tags, content, sig) + PublicMessageEvent.KIND -> PublicMessageEvent(id, pubKey, createdAt, tags, content, sig) ReactionEvent.KIND -> ReactionEvent(id, pubKey, createdAt, tags, content, sig) RelationshipStatusEvent.KIND -> RelationshipStatusEvent(id, pubKey, createdAt, tags, content, sig) RelayAuthEvent.KIND -> RelayAuthEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/PublicMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/PublicMessageEvent.kt new file mode 100644 index 0000000000..f688b42df5 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/PublicMessageEvent.kt @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.publicMessages + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.publicMessages.tags.ReceiverTag +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.core.any +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent +import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints +import com.vitorpamplona.quartz.nip19Bech32.pubKeys +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class PublicMessageEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), + PubKeyHintProvider, + SearchableEvent { + override fun indexableContent() = content + + override fun pubKeyHints() = tags.mapNotNull(ReceiverTag::parseAsHint) + citedNIP19().pubKeyHints() + + override fun linkedPubKeys() = tags.mapNotNull(ReceiverTag::parseKey) + citedNIP19().pubKeys() + + fun isIncluded(pubKey: HexKey) = tags.any(ReceiverTag::match, pubKey) || this.pubKey == pubKey + + fun group() = tags.mapNotNullTo(mutableListOf(ReceiverTag(this.pubKey, null)), ReceiverTag::parse) + + fun groupKeys() = tags.mapNotNullTo(mutableListOf(this.pubKey), ReceiverTag::parseKey) + + fun groupKeySet() = tags.mapNotNullTo(mutableSetOf(this.pubKey), ReceiverTag::parseKey) + + companion object { + const val KIND = 24 + const val ALT_DESCRIPTION = "Public Message" + + fun build( + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTION) + initializer() + } + + fun build( + to: ReceiverTag, + msg: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, msg, createdAt) { + alt(ALT_DESCRIPTION) + toUser(to) + initializer() + } + + fun build( + to: List, + msg: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, msg, createdAt) { + alt(ALT_DESCRIPTION) + toGroup(to) + initializer() + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..9370494dda --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/TagArrayBuilderExt.kt @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.publicMessages + +import com.vitorpamplona.quartz.experimental.publicMessages.tags.ReceiverTag +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder + +fun TagArrayBuilder.toGroup(list: List) = addAll(list.map { it.toTagArray() }) + +fun TagArrayBuilder.toUser(user: ReceiverTag) = add(user.toTagArray()) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/tags/ReceiverTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/tags/ReceiverTag.kt new file mode 100644 index 0000000000..7378e2086e --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/experimental/publicMessages/tags/ReceiverTag.kt @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.publicMessages.tags + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.toNpub +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.ensure + +class ReceiverTag( + val pubKey: HexKey, + val relayHint: NormalizedRelayUrl? = null, +) { + fun toNProfile(): String = NProfile.create(pubKey, relayHint?.let { listOf(it) } ?: emptyList()) + + fun toNPub(): String = pubKey.hexToByteArray().toNpub() + + fun toTagArray() = assemble(pubKey, relayHint) + + fun toTagIdOnly() = assemble(pubKey, null) + + companion object Companion { + const val TAG_NAME = "p" + + fun isTagged(tag: Array): Boolean = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64 + + fun match( + tag: Array, + key: HexKey, + ): Boolean = tag.has(1) && tag[0] == TAG_NAME && tag[1] == key + + @JvmStatic + fun parse(tag: Tag): ReceiverTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return ReceiverTag(tag[1], hint) + } + + @JvmStatic + fun parseKey(tag: Tag): HexKey? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + return tag[1] + } + + @JvmStatic + fun parseAsHint(tag: Tag): PubKeyHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + + ensure(hint != null) { return null } + + return PubKeyHint(tag[1], hint) + } + + @JvmStatic + fun assemble( + pubkey: HexKey, + relayHint: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint?.url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseNoteEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseNoteEvent.kt new file mode 100644 index 0000000000..8b02fc2bbb --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseNoteEvent.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip10Notes + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithEventsOrAddresses +import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithPeople +import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris +import com.vitorpamplona.quartz.nip19Bech32.entities.Entity +import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress +import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote +import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub + +@Immutable +open class BaseNoteEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + @Transient + private var citedUsersCache: Set? = null + + @Transient + private var citedNotesCache: Set? = null + + @Transient + private var citedNIP19Cache: List? = null + + fun citedNIP19(): List { + citedNIP19Cache?.let { + return it + } + + return findNostrUris(content).also { citedNIP19Cache = it } + } + + fun citedUsers(): Set { + citedUsersCache?.let { + return it + } + + val citedUsers = mutableSetOf() + + findIndexTagsWithPeople(content, tags, citedUsers) + citedNIP19().forEach { parsed -> + when (parsed) { + is NProfile -> citedUsers.add(parsed.hex) + is NPub -> citedUsers.add(parsed.hex) + } + } + + citedUsersCache = citedUsers + return citedUsers + } + + fun findCitations(): Set { + citedNotesCache?.let { + return it + } + + val citations = mutableSetOf() + + findIndexTagsWithEventsOrAddresses(content, tags, citations).toMutableSet() + citedNIP19().forEach { entity -> + when (entity) { + is NEvent -> citations.add(entity.hex) + is NAddress -> citations.add(entity.aTag()) + is NNote -> citations.add(entity.hex) + is NEmbed -> citations.add(entity.event.id) + } + } + + citedNotesCache = citations + return citations + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index d00610eae8..072cf6fbe4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -21,21 +21,10 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers -import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithEventsOrAddresses -import com.vitorpamplona.quartz.nip10Notes.content.findIndexTagsWithPeople -import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag -import com.vitorpamplona.quartz.nip19Bech32.entities.Entity -import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress -import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed -import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.nip19Bech32.entities.NNote -import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile -import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull @@ -49,16 +38,7 @@ open class BaseThreadedEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient - private var citedUsersCache: Set? = null - - @Transient - private var citedNotesCache: Set? = null - - @Transient - private var citedNIP19Cache: List? = null - +) : BaseNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun mentions() = taggedUsers() fun markedRoot() = tags.firstNotNullOfOrNull(MarkedETag::parseRoot) @@ -92,54 +72,6 @@ open class BaseThreadedEvent( return newStyleReply ?: newStyleRoot ?: oldStylePositional } - fun citedNIP19(): List { - citedNIP19Cache?.let { - return it - } - - return findNostrUris(content).also { citedNIP19Cache = it } - } - - fun citedUsers(): Set { - citedUsersCache?.let { - return it - } - - val citedUsers = mutableSetOf() - - findIndexTagsWithPeople(content, tags, citedUsers) - citedNIP19().forEach { parsed -> - when (parsed) { - is NProfile -> citedUsers.add(parsed.hex) - is NPub -> citedUsers.add(parsed.hex) - } - } - - citedUsersCache = citedUsers - return citedUsers - } - - fun findCitations(): Set { - citedNotesCache?.let { - return it - } - - val citations = mutableSetOf() - - findIndexTagsWithEventsOrAddresses(content, tags, citations).toMutableSet() - citedNIP19().forEach { entity -> - when (entity) { - is NEvent -> citations.add(entity.hex) - is NAddress -> citations.add(entity.aTag()) - is NNote -> citations.add(entity.hex) - is NEmbed -> citations.add(entity.event.id) - } - } - - citedNotesCache = citations - return citations - } - fun tagsWithoutCitations(): List { val certainRepliesTo = markedReplyTos() val uncertainRepliesTo = unmarkedReplyTos() From c9de32c3faf1f7235b131023090525e2dd47e979 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 29 Jul 2025 15:08:47 -0400 Subject: [PATCH 152/185] fixes voice track bugs --- .../com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt | 3 ++- .../com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt | 6 ++---- .../ui/screen/loggedIn/threadview/ThreadFeedView.kt | 3 ++- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt index bfce94baff..718b6e378a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AudioTrack.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.playback.composable.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.service.playback.composable.VideoView +import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor @@ -183,7 +184,7 @@ fun AudioHeader( nav: INav, ) { val media = remember { noteEvent.stream() ?: noteEvent.download() } - val waveform = remember { noteEvent.wavefrom() } + val waveform = remember { noteEvent.wavefrom()?.let { WaveformData(it.wave) } } val content = remember { noteEvent.content.ifBlank { null } } val defaultBackground = MaterialTheme.colorScheme.background diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 5233140938..a0ace2304a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -25,9 +25,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -51,11 +49,11 @@ fun RenderVoiceTrack( ) { val noteEvent = note.event as? BaseVoiceEvent ?: return - AudioVoiceHeader(noteEvent, note, contentScale, accountViewModel, nav) + VoiceHeader(noteEvent, note, contentScale, accountViewModel, nav) } @Composable -fun AudioVoiceHeader( +fun VoiceHeader( noteEvent: BaseVoiceEvent, note: Note, contentScale: ContentScale, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 06841d4214..318672bd9d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -154,6 +154,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderTextModificationEvent import com.vitorpamplona.amethyst.ui.note.types.RenderTorrent import com.vitorpamplona.amethyst.ui.note.types.RenderTorrentComment import com.vitorpamplona.amethyst.ui.note.types.VideoDisplay +import com.vitorpamplona.amethyst.ui.note.types.VoiceHeader import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -551,7 +552,7 @@ private fun FullBleedNoteCompose( } else if (noteEvent is PictureEvent) { PictureDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, PaddingValues(vertical = Size5dp), backgroundColor, accountViewModel = accountViewModel, nav) } else if (noteEvent is BaseVoiceEvent) { - VoiceEventDisplay(baseNote, makeItShort = false, canPreview = true, backgroundColor = backgroundColor, ContentScale.FillWidth, accountViewModel = accountViewModel, nav = nav) + VoiceHeader(noteEvent, baseNote, ContentScale.FillWidth, accountViewModel, nav) } else if (noteEvent is FileHeaderEvent) { FileHeaderDisplay(baseNote, roundedCorner = true, ContentScale.FillWidth, accountViewModel = accountViewModel) } else if (noteEvent is FileStorageHeaderEvent) { From 8abba9246cc51e3d23b601a652f96049318f7a8b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 29 Jul 2025 20:07:28 -0400 Subject: [PATCH 153/185] Adds support to reply to Yaks with another voice message. --- amethyst/src/main/AndroidManifest.xml | 3 + .../vitorpamplona/amethyst/model/Account.kt | 23 + .../amethyst/model/LocalCache.kt | 2 + .../ui/actions/uploads/RecordAudio.kt | 84 +++ .../ui/actions/uploads/TakePicture.kt | 2 +- .../actions/uploads/VoiceMessageRecorder.kt | 106 +++ .../amethyst/ui/components/ClickableBox.kt | 123 ++++ .../amethyst/ui/navigation/AppNavigation.kt | 9 + .../ui/navigation/routes/RouteMaker.kt | 2 + .../amethyst/ui/navigation/routes/Routes.kt | 10 + .../vitorpamplona/amethyst/ui/note/Icons.kt | 14 + .../amethyst/ui/note/ReactionsRow.kt | 36 +- .../ui/screen/loggedIn/AccountViewModel.kt | 57 +- .../publicMessages/NewPublicMessageScreen.kt | 379 +++++++++++ .../NewPublicMessageViewModel.kt | 644 ++++++++++++++++++ .../dal/UserProfileConversationsFeedFilter.kt | 2 + .../datasource/FilterUserProfilePosts.kt | 4 + .../dal/UserProfileNewThreadFeedFilter.kt | 2 + amethyst/src/main/res/values/strings.xml | 4 + .../quartz/nipA0VoiceMessages/AudioMeta.kt | 11 +- .../nipA0VoiceMessages/BaseVoiceEvent.kt | 1 + .../nipA0VoiceMessages/IMetaTagBuilderExt.kt | 3 + .../quartz/nipA0VoiceMessages/IMetaTagExt.kt | 3 + .../nipA0VoiceMessages/TagArrayBuilderExt.kt | 23 +- .../quartz/nipA0VoiceMessages/VoiceEvent.kt | 8 + .../nipA0VoiceMessages/VoiceReplyEvent.kt | 38 +- .../nipA0VoiceMessages/tags/MimeTypeTag.kt | 46 ++ .../nipA0VoiceMessages/tags/ReplyAuthorTag.kt | 85 +++ .../nipA0VoiceMessages/tags/ReplyEventTag.kt | 109 +++ .../nipA0VoiceMessages/tags/ReplyKindTag.kt | 76 +++ 30 files changed, 1899 insertions(+), 10 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessageRecorder.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/MimeTypeTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyAuthorTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyEventTag.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyKindTag.kt diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index f65b84ef8e..1adc0f6598 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -23,6 +23,9 @@ + + + diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index d19d585120..b805dc2c91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -204,6 +204,8 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag import com.vitorpamplona.quartz.nip98HttpAuth.HTTPAuthorizationEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers @@ -960,6 +962,27 @@ class Account( cache.getNoteIfExists(signedEvent.id)?.let { onReady(it) } } + suspend fun sendVoiceMessage( + url: String, + mimeType: String?, + hash: String, + duration: Int, + waveform: List, + ) { + signAndComputeBroadcast(VoiceEvent.build(url, mimeType, hash, duration, waveform)) + } + + suspend fun sendVoiceReplyMessage( + url: String, + mimeType: String?, + hash: String, + duration: Int, + waveform: List, + replyTo: EventHintBundle, + ) { + signAndComputeBroadcast(VoiceReplyEvent.build(url, mimeType, hash, duration, waveform, replyTo)) + } + suspend fun sendAllAsOnePictureEvent( urlHeaderInfo: Map, caption: String?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 8409dd9072..03372d5cde 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -782,6 +782,8 @@ object LocalCache : ILocalCache { is TextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } is CommentEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } + is VoiceReplyEvent -> event.markedReplyTos().mapNotNull { checkGetOrCreateNote(it) } + is ChatMessageEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } is ChatMessageEncryptedFileHeaderEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt new file mode 100644 index 0000000000..8f2eae9b8a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.uploads + +import android.Manifest +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.components.ClickAndHoldBoxComposable +import com.vitorpamplona.amethyst.ui.stringRes + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun RecordAudioBox( + modifier: Modifier, + onRecordTaken: (RecordingResult) -> Unit, + content: @Composable (Boolean) -> Unit, +) { + val mediaRecorder = remember { mutableStateOf(null) } + val context = LocalContext.current + + ClickAndHoldBoxComposable( + modifier = modifier, + onPress = { + val recordPermissionState = rememberPermissionState(Manifest.permission.RECORD_AUDIO) + val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + if (!recordPermissionState.status.isGranted) { + recordPermissionState.launchPermissionRequest() + } else { + mediaRecorder.value = VoiceMessageRecorder() + mediaRecorder.value?.start(context, scope) + } + } + }, + onRelease = { + val result = mediaRecorder.value?.stop() + if (result != null) { + onRecordTaken(result) + } else { + // less disruptive than error messages + Toast + .makeText( + context, + stringRes(context, R.string.record_a_message_description), + Toast.LENGTH_SHORT, + ).show() + } + }, + onCancel = { + mediaRecorder.value?.stop() + }, + content, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt index 047336d139..b5675ee8a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/TakePicture.kt @@ -129,7 +129,7 @@ fun PictureButton(onClick: () -> Unit) { ) { Icon( imageVector = Icons.Default.CameraAlt, - contentDescription = stringRes(id = R.string.upload_image), + contentDescription = stringRes(id = R.string.take_a_picture), modifier = Modifier.height(22.dp), tint = MaterialTheme.colorScheme.onBackground, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessageRecorder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessageRecorder.kt new file mode 100644 index 0000000000..ba98a19c45 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessageRecorder.kt @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.uploads + +import android.content.Context +import android.media.MediaRecorder +import android.os.Build +import androidx.media3.common.MimeTypes +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.io.File + +class RecordingResult( + val file: File, + val mimeType: String, + val amplitudes: List, + val duration: Int, +) + +class VoiceMessageRecorder { + private var recorder: MediaRecorder? = null + private var outputFile: File? = null + private var startTime: Long = 0 + private var job: Job? = null + private var amplitudes: MutableList = mutableListOf() + + private fun createRecorder(context: Context): MediaRecorder = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + MediaRecorder(context) + } else { + MediaRecorder() + } + + suspend fun start( + context: Context, + scope: CoroutineScope, + ) { + val fileName = RandomInstance.randomChars(16) + ".mp4" + val outputFile = File(context.cacheDir, "/voice/$fileName") + outputFile.parentFile?.mkdirs() + this.outputFile = outputFile + this.startTime = TimeUtils.now() + this.amplitudes.clear() + + createRecorder(context).apply { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) + setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + setOutputFile(outputFile) + + prepare() + start() + + recorder = this + } + + job?.cancel() + job = + scope.launch { + while (recorder != null) { + amplitudes.add(((recorder?.maxAmplitude ?: 0) / 100).toInt()) + delay(1000) + } + } + } + + suspend fun stop(): RecordingResult? { + recorder?.stop() + recorder?.reset() + recorder = null + val currentTime = TimeUtils.now() + val file = outputFile + return if (currentTime - startTime >= 1 && file != null) { + RecordingResult( + file, + MimeTypes.AUDIO_AAC, + amplitudes, + (currentTime - startTime).toInt(), + ) + } else { + null + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt index 90110d3027..2015fefcb5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt @@ -20,15 +20,29 @@ */ package com.vitorpamplona.amethyst.ui.components +import android.R.attr.onClick +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale import androidx.compose.ui.semantics.Role import com.vitorpamplona.amethyst.ui.theme.ripple24dp @@ -51,6 +65,115 @@ fun ClickableBox( } } +@Composable +fun ClickAndHoldBox( + modifier: Modifier = Modifier, + onPress: () -> Unit, + onRelease: () -> Unit, + content: @Composable (Boolean) -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + LaunchedEffect(isPressed) { + if (isPressed) { + // Button is pressed + onPress() + } else { + // Button is released + onRelease() + } + } + + // Animation for the button scale + val scale by animateFloatAsState( + targetValue = if (isPressed) 1.5f else 1.0f, // Scale up when recording + animationSpec = tween(durationMillis = 150), // Smooth animation + ) + + // Animation for the button color + val backgroundColor by animateColorAsState( + targetValue = if (isPressed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background, + animationSpec = tween(durationMillis = 150), + ) + + Box( + modifier + .scale(scale) + .background(backgroundColor, CircleShape) + .clickable( + role = Role.Button, + interactionSource = interactionSource, + indication = ripple24dp, + onClick = { }, + ), + contentAlignment = Alignment.Center, + ) { + content(isPressed) + } +} + +@Composable +fun ClickAndHoldBoxComposable( + modifier: Modifier = Modifier, + onPress: @Composable () -> Unit, + onRelease: suspend () -> Unit, + onCancel: suspend () -> Unit, + content: @Composable (Boolean) -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + var isPressed by remember { mutableStateOf(false) } + + if (isPressed) { + onPress() + } + + LaunchedEffect(interactionSource) { + val pressInteractions = mutableListOf() + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> pressInteractions.add(interaction) + is PressInteraction.Release -> { + onRelease() + pressInteractions.remove(interaction.press) + } + is PressInteraction.Cancel -> { + onCancel() + pressInteractions.remove(interaction.press) + } + } + isPressed = pressInteractions.isNotEmpty() + } + } + + // Animation for the button scale + val scale by animateFloatAsState( + targetValue = if (isPressed) 1.5f else 1.0f, // Scale up when recording + animationSpec = tween(durationMillis = 150), // Smooth animation + ) + + // Animation for the button color + val backgroundColor by animateColorAsState( + targetValue = if (isPressed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background, + animationSpec = tween(durationMillis = 150), + ) + + Box( + modifier + .scale(scale) + .background(backgroundColor, CircleShape) + .clickable( + role = Role.Button, + interactionSource = interactionSource, + indication = ripple24dp, + onClick = { }, + ), + contentAlignment = Alignment.Center, + ) { + content(isPressed) + } +} + @OptIn(ExperimentalFoundationApi::class) @Composable fun ClickableBox( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index bfb98950e8..89fefa6780 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -78,6 +78,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.redirect.LoadRedirectScreen @@ -172,6 +173,14 @@ fun AppNavigation( ) } + composableFromBottomArgs { + NewPublicMessageScreen( + to = it.toKey(), + accountViewModel, + nav, + ) + } + composableFromBottomArgs { HashtagPostScreen( hashtag = it.hashtag, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 706f2503ab..a51df64409 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -200,6 +201,7 @@ fun routeReplyTo( ): Route? { val noteEvent = note.event return when (noteEvent) { + is PublicMessageEvent -> Route.NewPublicMessage(noteEvent.groupKeySet() - account.userProfile().pubkeyHex) is TextNoteEvent -> Route.NewPost(baseReplyTo = note.idHex) is PrivateDmEvent -> routeToMessage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index c62974fb8c..0dbc6c0a33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -138,6 +138,16 @@ sealed class Route { fun toKey(): ChatroomKey = ChatroomKey(id.split(",").toSet()) } + @Serializable data class NewPublicMessage( + val to: String, + ) : Route() { + constructor(users: Set) : this( + to = users.joinToString(","), + ) + + fun toKey(): Set = to.split(",").toSet() + } + @Serializable data class RoomByAuthor( val id: String, ) : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index d331ef3544..6bd827c590 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -39,6 +39,7 @@ import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.outlined.AddReaction import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Mic import androidx.compose.material.icons.outlined.PlayCircle import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -314,6 +315,19 @@ fun ExpandMoreIcon( ) } +@Composable +fun VoiceReplyIcon( + iconSizeModifier: Modifier, + tint: Color, +) { + Icon( + imageVector = Icons.Outlined.Mic, + contentDescription = stringRes(id = R.string.record_a_message), + tint = tint, + modifier = iconSizeModifier, + ) +} + @Composable fun CommentIcon( iconSizeModifier: Modifier, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index b40b7357bd..485d665cd9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -93,6 +93,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.amethyst.model.FeatureSetType @@ -109,6 +110,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.GenericLoadable @@ -156,6 +158,7 @@ import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount +import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf @@ -578,8 +581,37 @@ private fun ReplyReactionWithDialog( accountViewModel: AccountViewModel, nav: INav, ) { - ReplyReaction(baseNote, grayTint, accountViewModel) { - nav.nav { routeReplyTo(baseNote, accountViewModel.account) } + if (baseNote.event is BaseVoiceEvent) { + ReplyViaVoiceReaction(baseNote, grayTint, accountViewModel) + } else { + ReplyReaction(baseNote, grayTint, accountViewModel) { + nav.nav { routeReplyTo(baseNote, accountViewModel.account) } + } + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun ReplyViaVoiceReaction( + baseNote: Note, + grayTint: Color, + accountViewModel: AccountViewModel, + showCounter: Boolean = true, + iconSizeModifier: Modifier = Size19Modifier, +) { + val context = LocalContext.current + + RecordAudioBox( + modifier = iconSizeModifier, + onRecordTaken = { audio -> + accountViewModel.sendVoiceReply(baseNote, audio, context) + }, + ) { + VoiceReplyIcon(iconSizeModifier, grayTint) + } + + if (showCounter) { + ReplyCounter(baseNote, grayTint, accountViewModel) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 9b72ca41be..690928c228 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -28,6 +28,7 @@ import android.util.LruCache import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable +import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -65,8 +66,12 @@ import com.vitorpamplona.amethyst.service.cashu.CashuToken import com.vitorpamplona.amethyst.service.cashu.melt.MeltProcessor import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver +import com.vitorpamplona.amethyst.service.uploads.CompressorQuality +import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk +import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager import com.vitorpamplona.amethyst.ui.feeds.FeedState @@ -104,6 +109,7 @@ import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay @@ -123,6 +129,7 @@ import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.mapNotNullAsync @@ -1249,6 +1256,54 @@ class AccountViewModel( super.onCleared() } + fun sendVoiceReply( + note: Note, + recording: RecordingResult, + context: Context, + ) { + if (isWriteable()) { + val hint = note.toEventHint() + if (hint == null) return + + runIOCatching { + val uploader = UploadOrchestrator() + val result = + uploader.upload( + uri = recording.file.toUri(), + mimeType = recording.mimeType, + alt = null, + contentWarningReason = null, + compressionQuality = CompressorQuality.UNCOMPRESSED, + server = account.settings.defaultFileServer, + account = account, + context = context, + ) + + if (result is UploadingState.Finished && result.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + account.sendVoiceReplyMessage( + result.result.url, + result.result.fileHeader.mimeType ?: recording.mimeType, + result.result.fileHeader.hash, + recording.duration, + recording.amplitudes, + hint, + ) + } else if (result is UploadingState.Error) { + toastManager.toast( + R.string.failed_to_upload_media_no_details, + result.errorResource, + *result.params, + ) + } + } + } else { + toastManager.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_reply, + ) + } + } + fun loadThumb( context: Context, thumbUri: String, @@ -1673,7 +1728,7 @@ class AccountViewModel( is NSec -> {} is NPub -> {} is NProfile -> {} - is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> { + is NNote -> { LocalCache.checkGetOrCreateNote(parsed.hex)?.let { note -> returningNote = note } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt new file mode 100644 index 0000000000..f8279cc1f5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -0,0 +1,379 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation +import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery +import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton +import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.navs.Nav +import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar +import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer +import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton +import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList +import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton +import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest +import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton +import com.vitorpamplona.amethyst.ui.note.creators.location.LocationAsHash +import com.vitorpamplona.amethyst.ui.note.creators.previews.DisplayPreviews +import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.AddSecretEmojiButton +import com.vitorpamplona.amethyst.ui.note.creators.secretEmoji.SecretEmojiRequest +import com.vitorpamplona.amethyst.ui.note.creators.uploads.ImageVideoDescription +import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList +import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.AddZapraiserButton +import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest +import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo +import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.MessageFieldRow +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.SendDirectMessageTo +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.Font14SP +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) +@Composable +fun NewPublicMessageScreen( + to: Set? = null, + accountViewModel: AccountViewModel, + nav: Nav, +) { + val postViewModel: NewPublicMessageViewModel = viewModel() + postViewModel.init(accountViewModel) + + LaunchedEffect(Unit) { + withContext(Dispatchers.IO) { + to?.let { + postViewModel.load(it) + } + } + } + + WatchAndLoadMyEmojiList(accountViewModel) + + Scaffold( + topBar = { + PostingTopBar( + titleRes = R.string.public_message, + isActive = postViewModel::canPost, + onCancel = { + // uses the accountViewModel scope to avoid cancelling this + // function when the postViewModel is released + accountViewModel.runIOCatching { + postViewModel.sendDraftSync() + delay(100) + nav.popBack() + postViewModel.cancel() + } + }, + onPost = { + // uses the accountViewModel scope to avoid cancelling this + // function when the postViewModel is released + accountViewModel.runIOCatching { + postViewModel.sendPostSync() + nav.popBack() + postViewModel.cancel() + } + nav.popBack() + }, + ) + }, + ) { pad -> + Surface( + modifier = + Modifier + .padding(pad) + .consumeWindowInsets(pad) + .imePadding(), + ) { + PublicMessageScreenContent(postViewModel, accountViewModel, nav) + } + } +} + +@Composable +fun PublicMessageScreenContent( + postViewModel: NewPublicMessageViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + val scrollState = rememberScrollState() + + Column(Modifier.fillMaxSize()) { + Row(Modifier.fillMaxWidth().padding(horizontal = Size10dp).weight(1f)) { + Column( + Modifier.fillMaxWidth().verticalScroll(scrollState), + verticalArrangement = spacedBy(Size10dp), + ) { + SendDirectMessageTo(postViewModel, accountViewModel) + + MessageFieldRow(postViewModel, accountViewModel) + + DisplayPreviews(postViewModel.urlPreviews, accountViewModel, nav) + + if (postViewModel.wantsToMarkAsSensitive) { + ContentSensitivityExplainer() + } + + if (postViewModel.wantsToAddGeoHash) { + LocationAsHash(postViewModel) + } + + if (postViewModel.wantsForwardZapTo) { + ForwardZapTo(postViewModel, accountViewModel) + } + + postViewModel.multiOrchestrator?.let { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + val context = LocalContext.current + ImageVideoDescription( + it, + accountViewModel.account.settings.defaultFileServer, + onAdd = { alt, server, sensitiveContent, mediaQuality -> + postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) + if (server.type != ServerType.NIP95) { + accountViewModel.account.settings.changeDefaultFileServer(server) + } + }, + onDelete = postViewModel::deleteMediaToUpload, + onCancel = { postViewModel.multiOrchestrator = null }, + accountViewModel = accountViewModel, + ) + } + } + + if (postViewModel.wantsInvoice) { + NewPostInvoiceRequest( + onSuccess = { + postViewModel.insertAtCursor(it) + postViewModel.wantsInvoice = false + }, + accountViewModel, + ) + } + + if (postViewModel.wantsSecretEmoji) { + SecretEmojiRequest { + postViewModel.insertAtCursor(it) + postViewModel.wantsSecretEmoji = false + } + } + + if (postViewModel.wantsZapraiser && postViewModel.hasLnAddress()) { + ZapRaiserRequest( + stringRes(id = R.string.zapraiser), + postViewModel, + ) + } + } + } + + postViewModel.userSuggestions?.let { + ShowUserSuggestionList( + it, + postViewModel::autocompleteWithUser, + accountViewModel, + Modifier.heightIn(0.dp, 300.dp), + ) + } + + postViewModel.emojiSuggestions?.let { + ShowEmojiSuggestionList( + it, + postViewModel::autocompleteWithEmoji, + postViewModel::autocompleteWithEmojiUrl, + accountViewModel, + Modifier.heightIn(0.dp, 300.dp), + ) + } + + BottomRowActions(postViewModel, accountViewModel) + } +} + +@Composable +private fun BottomRowActions( + postViewModel: NewPublicMessageViewModel, + accountViewModel: AccountViewModel, +) { + val scrollState = rememberScrollState() + Row( + modifier = + Modifier + .horizontalScroll(scrollState) + .fillMaxWidth() + .height(50.dp), + verticalAlignment = CenterVertically, + ) { + SelectFromGallery( + isUploading = postViewModel.isUploadingImage, + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier, + ) { + postViewModel.selectImage(it) + } + + TakePictureButton( + onPictureTaken = { + postViewModel.selectImage(it) + }, + ) + + ForwardZapToButton(postViewModel.wantsForwardZapTo) { + postViewModel.wantsForwardZapTo = !postViewModel.wantsForwardZapTo + } + + if (postViewModel.canAddZapRaiser) { + AddZapraiserButton(postViewModel.wantsZapraiser) { + postViewModel.wantsZapraiser = !postViewModel.wantsZapraiser + } + } + + MarkAsSensitiveButton(postViewModel.wantsToMarkAsSensitive) { + postViewModel.toggleMarkAsSensitive() + } + + AddGeoHashButton(postViewModel.wantsToAddGeoHash) { + postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash + } + + AddSecretEmojiButton(postViewModel.wantsSecretEmoji) { + postViewModel.wantsSecretEmoji = !postViewModel.wantsSecretEmoji + } + + if (postViewModel.canAddInvoice && postViewModel.hasLnAddress()) { + AddLnInvoiceButton(postViewModel.wantsInvoice) { + postViewModel.wantsInvoice = !postViewModel.wantsInvoice + } + } + } +} + +@Composable +fun SendDirectMessageTo( + postViewModel: NewPublicMessageViewModel, + accountViewModel: AccountViewModel, +) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + LaunchedEffect(Unit) { + launch { + delay(200) + focusRequester.requestFocus() + } + } + + Column(Modifier.fillMaxWidth()) { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringRes(R.string.messages_new_message_to), + fontSize = Font14SP, + fontWeight = FontWeight.W500, + ) + + ThinPaddingTextField( + value = postViewModel.toUsers, + onValueChange = postViewModel::updateToUsers, + modifier = + Modifier + .weight(1f) + .focusRequester(focusRequester) + .onFocusChanged { + if (it.isFocused) { + keyboardController?.show() + } + }, + placeholder = { + Text( + text = stringRes(R.string.messages_new_message_to_caption), + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + visualTransformation = + UrlUserTagTransformation( + MaterialTheme.colorScheme.primary, + ), + colors = + OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = Color.Transparent, + focusedBorderColor = Color.Transparent, + ), + ) + } + + HorizontalDivider(thickness = DividerThickness) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt new file mode 100644 index 0000000000..f42093791c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -0,0 +1,644 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages + +import android.content.Context +import android.util.Log +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.compose.currentWord +import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor +import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState +import com.vitorpamplona.amethyst.service.location.LocationState +import com.vitorpamplona.amethyst.service.uploads.MediaCompressor +import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator +import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator +import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger +import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia +import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState +import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber +import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField +import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState +import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.IZapRaiser +import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.IZapField +import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.SplitBuilder +import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.toZapSplitSetup +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent +import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent +import com.vitorpamplona.quartz.experimental.publicMessages.tags.ReceiverTag +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash +import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags +import com.vitorpamplona.quartz.nip01Core.tags.references.references +import com.vitorpamplona.quartz.nip10Notes.content.findHashtags +import com.vitorpamplona.quartz.nip10Notes.content.findNostrUris +import com.vitorpamplona.quartz.nip10Notes.content.findURLs +import com.vitorpamplona.quartz.nip18Reposts.quotes.quotes +import com.vitorpamplona.quartz.nip18Reposts.quotes.taggedQuoteIds +import com.vitorpamplona.quartz.nip19Bech32.toNpub +import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag +import com.vitorpamplona.quartz.nip30CustomEmoji.emojis +import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning +import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive +import com.vitorpamplona.quartz.nip37Drafts.DraftEvent +import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup +import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup +import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits +import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser +import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount +import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder +import com.vitorpamplona.quartz.nip92IMeta.imetas +import com.vitorpamplona.quartz.nip94FileMetadata.alt +import com.vitorpamplona.quartz.nip94FileMetadata.blurhash +import com.vitorpamplona.quartz.nip94FileMetadata.dims +import com.vitorpamplona.quartz.nip94FileMetadata.hash +import com.vitorpamplona.quartz.nip94FileMetadata.magnet +import com.vitorpamplona.quartz.nip94FileMetadata.mimeType +import com.vitorpamplona.quartz.nip94FileMetadata.originalHash +import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent +import com.vitorpamplona.quartz.nip94FileMetadata.size +import com.vitorpamplona.quartz.utils.Hex +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlin.collections.plus + +@Stable +class NewPublicMessageViewModel : + ViewModel(), + ILocationGrabber, + IMessageField, + IZapField, + IZapRaiser { + val draftTag = DraftTagState() + + lateinit var accountViewModel: AccountViewModel + lateinit var account: Account + + init { + viewModelScope.launch(Dispatchers.IO) { + draftTag.versions.collectLatest { + // don't save the first + if (it > 0) { + sendDraftSync() + } + } + } + } + + val iMetaAttachments = IMetaAttachments() + var nip95attachments by mutableStateOf>>(emptyList()) + + override var message by mutableStateOf(TextFieldValue("")) + + val urlPreviews = PreviewState() + + var isUploadingImage by mutableStateOf(false) + + var userSuggestions: UserSuggestionState? = null + var userSuggestionsMainMessage: UserSuggestionAnchor? = null + + var emojiSuggestions: EmojiSuggestionState? = null + + var toUsers by mutableStateOf(TextFieldValue("")) + + // Images and Videos + var multiOrchestrator by mutableStateOf(null) + + // Invoices + var canAddInvoice by mutableStateOf(false) + var wantsInvoice by mutableStateOf(false) + + var wantsSecretEmoji by mutableStateOf(false) + + // Forward Zap to + var wantsForwardZapTo by mutableStateOf(false) + override val forwardZapTo = mutableStateOf>(SplitBuilder()) + override val forwardZapToEditting = mutableStateOf(TextFieldValue("")) + + // NSFW, Sensitive + var wantsToMarkAsSensitive by mutableStateOf(false) + + // GeoHash + var wantsToAddGeoHash by mutableStateOf(false) + var location: StateFlow? = null + + // ZapRaiser + var canAddZapRaiser by mutableStateOf(false) + var wantsZapraiser by mutableStateOf(false) + override var zapRaiserAmount = mutableStateOf(null) + + fun lnAddress(): String? = account.userProfile().info?.lnAddress() + + fun hasLnAddress(): Boolean = account.userProfile().info?.lnAddress() != null + + fun user(): User? = account.userProfile() + + fun init(accountVM: AccountViewModel) { + this.accountViewModel = accountVM + this.account = accountVM.account + this.canAddInvoice = hasLnAddress() + this.canAddZapRaiser = hasLnAddress() + + this.userSuggestions?.reset() + this.userSuggestions = UserSuggestionState(accountVM) + + this.emojiSuggestions?.reset() + this.emojiSuggestions = EmojiSuggestionState(accountVM) + } + + fun load(users: Set) { + val userSet = users - account.userProfile().pubkeyHex + + toUsers = + TextFieldValue( + userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" }, + ) + } + + fun quote(quote: Note) { + message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}") + urlPreviews.update(message) + + // creates a split with that author. + val quotedAuthor = quote.author ?: return + + if (quotedAuthor.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { + if (forwardZapTo.value.items.none { it.key.pubkeyHex == quotedAuthor.pubkeyHex }) { + forwardZapTo.value.addItem(quotedAuthor) + } + if (forwardZapTo.value.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) { + forwardZapTo.value.addItem(accountViewModel.userProfile()) + } + + val pos = forwardZapTo.value.items.indexOfFirst { it.key.pubkeyHex == quotedAuthor.pubkeyHex } + forwardZapTo.value.updatePercentage(pos, 0.9f) + + wantsForwardZapTo = true + } + } + + fun editFromDraft(draft: Note) { + val noteEvent = draft.event + val noteAuthor = draft.author + + if (noteEvent is DraftEvent && noteAuthor != null) { + viewModelScope.launch(Dispatchers.IO) { + accountViewModel.createTempDraftNote(noteEvent)?.let { innerNote -> + val oldTag = (draft.event as? AddressableEvent)?.dTag() + if (oldTag != null) { + draftTag.set(oldTag) + } + loadFromDraft(innerNote) + } + } + } + } + + private fun loadFromDraft(draft: Note) { + val draftEvent = draft.event as? PublicMessageEvent ?: return + + val localForwardZapTo = draftEvent.tags.zapSplitSetup() + val totalWeight = localForwardZapTo.sumOf { it.weight } + forwardZapTo.value = SplitBuilder() + localForwardZapTo.forEach { + if (it is ZapSplitSetup) { + val user = LocalCache.getOrCreateUser(it.pubKeyHex) + forwardZapTo.value.addItem(user, (it.weight / totalWeight).toFloat()) + } + // don't support editing old-style splits. + } + forwardZapToEditting.value = TextFieldValue("") + wantsForwardZapTo = localForwardZapTo.isNotEmpty() + + wantsToMarkAsSensitive = draftEvent.isSensitive() + + val geohash = draftEvent.getGeoHash() + wantsToAddGeoHash = geohash != null + + val zapraiser = draftEvent.zapraiserAmount() + wantsZapraiser = zapraiser != null + zapRaiserAmount.value = null + if (zapraiser != null) { + zapRaiserAmount.value = zapraiser + } + + if (forwardZapTo.value.items.isNotEmpty()) { + wantsForwardZapTo = true + } + + val userSet = draftEvent.groupKeys() - account.userProfile().pubkeyHex + + toUsers = + TextFieldValue( + userSet.mapNotNull { runCatching { Hex.decode(it).toNpub() }.getOrNull() }.joinToString(", ") { "@$it" }, + ) + + message = TextFieldValue(draftEvent.content) + urlPreviews.update(message) + + iMetaAttachments.addAll(draftEvent.imetas()) + } + + override fun locationFlow(): StateFlow { + if (location == null) { + location = locationManager().geohashStateFlow + } + + return location!! + } + + suspend fun sendPostSync() { + val template = createTemplate() ?: return + val extraNotesToBroadcast = mutableListOf() + + if (nip95attachments.isNotEmpty()) { + val usedImages = template.tags.taggedQuoteIds().toSet() + nip95attachments.forEach { + if (usedImages.contains(it.second.id)) { + extraNotesToBroadcast.add(it.first) + extraNotesToBroadcast.add(it.second) + } + } + } + + accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast) + accountViewModel.deleteDraft(draftTag.current) + + cancel() + } + + suspend fun sendDraftSync() { + val accountViewModel = accountViewModel + + if (message.text.isBlank()) { + accountViewModel.account.deleteDraft(draftTag.current) + } else { + val template = createTemplate() ?: return + accountViewModel.account.createAndSendDraft(draftTag.current, template) + + nip95attachments.forEach { + account.sendToPrivateOutboxAndLocal(it.first) + account.sendToPrivateOutboxAndLocal(it.second) + } + } + } + + private suspend fun createTemplate(): EventTemplate? { + val toUsersTagger = NewMessageTagger(this@NewPublicMessageViewModel.toUsers.text, null, null, accountViewModel) + toUsersTagger.run() + + val tagger = NewMessageTagger(message.text, null, null, accountViewModel) + tagger.run() + + val users = (toUsersTagger.pTags ?: emptyList()) + (tagger.pTags ?: emptyList()) + val toUsers = users.mapTo(mutableSetOf()) { ReceiverTag(it.pubkeyHex, it.bestRelayHint()) } + + val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null + + val geoHash = (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() + val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null + + val emojis = findEmoji(tagger.message, account.emoji.myEmojis.value) + val urls = findURLs(tagger.message) + val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet()) + + val contentWarningReason = if (wantsToMarkAsSensitive) "" else null + + return PublicMessageEvent.build( + to = toUsers.toList(), + msg = tagger.message, + ) { + hashtags(findHashtags(tagger.message)) + references(findURLs(tagger.message)) + quotes(findNostrUris(tagger.message)) + + geoHash?.let { geohash(it) } + localZapRaiserAmount?.let { zapraiser(it) } + zapReceiver?.let { zapSplits(it) } + contentWarningReason?.let { contentWarning(it) } + + emojis(emojis) + imetas(usedAttachments) + } + } + + fun findEmoji( + message: String, + myEmojiSet: List?, + ): List { + if (myEmojiSet == null) return emptyList() + return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + } + } + + fun upload( + alt: String?, + contentWarningReason: String?, + mediaQuality: Int, + server: ServerName, + onError: (title: String, message: String) -> Unit, + context: Context, + ) = try { + uploadUnsafe(alt, contentWarningReason, mediaQuality, server, onError, context) + } catch (e: SignerExceptions.ReadOnlyException) { + onError( + stringRes(context, R.string.read_only_user), + stringRes(context, R.string.login_with_a_private_key_to_be_able_to_sign_events), + ) + } + + fun uploadUnsafe( + alt: String?, + contentWarningReason: String?, + mediaQuality: Int, + server: ServerName, + onError: (title: String, message: String) -> Unit, + context: Context, + ) { + viewModelScope.launch(Dispatchers.Default) { + val myMultiOrchestrator = multiOrchestrator ?: return@launch + + isUploadingImage = true + + val results = + myMultiOrchestrator.upload( + alt, + contentWarningReason, + MediaCompressor.intToCompressorQuality(mediaQuality), + server, + account, + context, + ) + + if (results.allGood) { + results.successful.forEach { state -> + if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { + val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) + nip95attachments = nip95attachments + nip95 + val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } + + note?.let { + message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) + urlPreviews.update(message) + } + } else if (state.result is UploadOrchestrator.OrchestratorResult.ServerResult) { + val iMeta = + IMetaTagBuilder(state.result.url) + .apply { + hash(state.result.fileHeader.hash) + size(state.result.fileHeader.size) + state.result.fileHeader.mimeType + ?.let { mimeType(it) } + state.result.fileHeader.dim + ?.let { dims(it) } + state.result.fileHeader.blurHash + ?.let { blurhash(it.blurhash) } + state.result.magnet?.let { magnet(it) } + state.result.uploadedHash?.let { originalHash(it) } + + alt?.let { alt(it) } + contentWarningReason?.let { sensitiveContent(contentWarningReason) } + }.build() + + iMetaAttachments.replace(iMeta.url, iMeta) + + message = message.insertUrlAtCursor(state.result.url) + urlPreviews.update(message) + } + } + + multiOrchestrator = null + } else { + val errorMessages = results.errors.map { stringRes(context, it.errorResource, *it.params) }.distinct() + onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) + } + + isUploadingImage = false + } + } + + fun cancel() { + toUsers = TextFieldValue("") + message = TextFieldValue("") + multiOrchestrator = null + + wantsInvoice = false + wantsZapraiser = false + zapRaiserAmount.value = null + + wantsForwardZapTo = false + wantsToMarkAsSensitive = false + wantsToAddGeoHash = false + wantsSecretEmoji = false + + forwardZapTo.value = SplitBuilder() + forwardZapToEditting.value = TextFieldValue("") + + urlPreviews.reset() + + userSuggestions?.reset() + userSuggestionsMainMessage = null + + iMetaAttachments.reset() + + emojiSuggestions?.reset() + + draftTag.rotate() + } + + fun deleteMediaToUpload(selected: SelectedMediaProcessing) { + this.multiOrchestrator?.remove(selected) + } + + fun addToMessage(it: String) { + updateMessage(TextFieldValue(message.text + " " + it)) + } + + override fun updateMessage(newMessage: TextFieldValue) { + message = newMessage + urlPreviews.update(newMessage) + + if (message.selection.collapsed) { + userSuggestionsMainMessage = UserSuggestionAnchor.MAIN_MESSAGE + + val lastWord = message.currentWord() + userSuggestions?.processCurrentWord(lastWord) + emojiSuggestions?.processCurrentWord(lastWord) + } + + draftTag.newVersion() + } + + fun updateToUsers(newToUsersValue: TextFieldValue) { + toUsers = newToUsersValue + + if (newToUsersValue.selection.collapsed) { + val lastWord = newToUsersValue.currentWord() + userSuggestionsMainMessage = UserSuggestionAnchor.TO_USERS + userSuggestions?.processCurrentWord(lastWord) + } + + draftTag.newVersion() + } + + override fun updateZapForwardTo(newZapForwardTo: TextFieldValue) { + forwardZapToEditting.value = newZapForwardTo + if (newZapForwardTo.selection.collapsed) { + val lastWord = newZapForwardTo.text + userSuggestionsMainMessage = UserSuggestionAnchor.FORWARD_ZAPS + userSuggestions?.processCurrentWord(lastWord) + } + } + + fun autocompleteWithUser(item: User) { + userSuggestions?.let { userSuggestions -> + if (userSuggestionsMainMessage == UserSuggestionAnchor.MAIN_MESSAGE) { + val lastWord = message.currentWord() + message = userSuggestions.replaceCurrentWord(message, lastWord, item) + urlPreviews.update(message) + } else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) { + forwardZapTo.value.addItem(item) + forwardZapToEditting.value = TextFieldValue("") + } else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) { + val lastWord = toUsers.currentWord() + toUsers = userSuggestions.replaceCurrentWord(toUsers, lastWord, item) + } + + userSuggestionsMainMessage = null + userSuggestions.reset() + } + + draftTag.newVersion() + } + + fun autocompleteWithEmoji(item: EmojiPackState.EmojiMedia) { + val wordToInsert = ":${item.code}:" + + message = message.replaceCurrentWord(wordToInsert) + urlPreviews.update(message) + + emojiSuggestions?.reset() + + draftTag.newVersion() + } + + fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { + val wordToInsert = item.link.url + " " + + viewModelScope.launch(Dispatchers.IO) { + iMetaAttachments.downloadAndPrepare(item.link.url) { + Amethyst.instance.okHttpClients.getHttpClient(accountViewModel.account.privacyState.shouldUseTorForImageDownload(item.link.url)) + } + } + + message = message.replaceCurrentWord(wordToInsert) + urlPreviews.update(message) + + emojiSuggestions?.reset() + + draftTag.newVersion() + } + + fun canPost(): Boolean = + message.text.isNotBlank() && + !isUploadingImage && + !wantsInvoice && + (!wantsZapraiser || zapRaiserAmount.value != null) && + (toUsers.text.isNotBlank()) && + multiOrchestrator == null + + fun insertAtCursor(newElement: String) { + message = message.insertUrlAtCursor(newElement) + } + + fun selectImage(uris: ImmutableList) { + multiOrchestrator = MultiOrchestrator(uris) + } + + override fun onCleared() { + super.onCleared() + Log.d("Init", "OnCleared: ${this.javaClass.simpleName}") + } + + override fun updateZapPercentage( + index: Int, + sliderValue: Float, + ) { + forwardZapTo.value.updatePercentage(index, sliderValue) + + draftTag.newVersion() + } + + override fun updateZapFromText() { + viewModelScope.launch(Dispatchers.Default) { + val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel) + tagger.run() + tagger.pTags?.forEach { taggedUser -> + if (!forwardZapTo.value.items.any { it.key == taggedUser }) { + forwardZapTo.value.addItem(taggedUser) + } + } + } + } + + override fun updateZapRaiserAmount(newAmount: Long?) { + zapRaiserAmount.value = newAmount + draftTag.newVersion() + } + + fun toggleMarkAsSensitive() { + wantsToMarkAsSensitive = !wantsToMarkAsSensitive + draftTag.newVersion() + } + + override fun locationManager(): LocationState = Amethyst.instance.locationManager +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt index ddef713b37..8cfa16a104 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/dal/UserProfileConversationsFeedFilter.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.nip22Comments.CommentEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent class UserProfileConversationsFeedFilter( val user: User, @@ -65,6 +66,7 @@ class UserProfileConversationsFeedFilter( it.event is ChannelMessageEvent || it.event is LiveActivitiesChatMessageEvent || it.event is CommentEvent || + it.event is VoiceReplyEvent || it.event is TorrentCommentEvent ) && !it.isNewThread() && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt index 24e3068655..05f1534b0e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt @@ -36,6 +36,8 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent val UserProfilePostKinds1 = listOf( @@ -47,6 +49,7 @@ val UserProfilePostKinds1 = PollNoteEvent.KIND, HighlightEvent.KIND, WikiNoteEvent.KIND, + VoiceEvent.KIND, ) val UserProfilePostKinds2 = @@ -55,6 +58,7 @@ val UserProfilePostKinds2 = TorrentCommentEvent.KIND, InteractiveStoryPrologueEvent.KIND, CommentEvent.KIND, + VoiceReplyEvent.KIND, ) fun filterUserProfilePosts( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt index 8e79370d70..0678a0564f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent class UserProfileNewThreadFeedFilter( val user: User, @@ -79,6 +80,7 @@ class UserProfileNewThreadFeedFilter( it.event is InteractiveStoryPrologueEvent || it.event is AudioTrackEvent || it.event is AudioHeaderEvent || + it.event is VoiceEvent || it.event is TorrentEvent ) && it.isNewThread() && diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 3ccb82c9b5..b6517ec190 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -159,6 +159,10 @@ Video saved to the phone\'s video gallery Failed to save the video Upload Image + Take a picture + Record a message + Record a message + Click and hold to record a message Uploading… User does not have a lightning address set up to receive sats "reply here.. " diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/AudioMeta.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/AudioMeta.kt index c74d41ff63..8d035e7206 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/AudioMeta.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/AudioMeta.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.WaveformTag data class AudioMeta( val url: String, + val mimeType: String? = null, val hash: String? = null, val duration: Int? = null, val waveform: List? = null, @@ -33,6 +34,7 @@ data class AudioMeta( fun toIMetaArray(): Array = IMetaTagBuilder(url) .apply { + mimeType?.let { mimeType(it) } hash?.let { hash(it) } duration?.let { duration(it) } waveform?.let { waveform(it) } @@ -42,10 +44,11 @@ data class AudioMeta( companion object { fun parse(iMeta: IMetaTag): AudioMeta = AudioMeta( - iMeta.url, - iMeta.hash()?.firstOrNull(), - iMeta.duration()?.firstOrNull()?.toIntOrNull(), - iMeta.waveform()?.firstOrNull()?.let { WaveformTag.parseWave(it) }, + url = iMeta.url, + mimeType = iMeta.mimeType()?.firstOrNull(), + hash = iMeta.hash()?.firstOrNull(), + duration = iMeta.duration()?.firstOrNull()?.toIntOrNull(), + waveform = iMeta.waveform()?.firstOrNull()?.let { WaveformTag.parseWave(it) }, ) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/BaseVoiceEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/BaseVoiceEvent.kt index 6fb98c5b9d..d6689ad5f2 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/BaseVoiceEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/BaseVoiceEvent.kt @@ -50,6 +50,7 @@ open class BaseVoiceEvent( initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(kind, voiceMessage.url, createdAt) { alt(alt) + audioIMeta(voiceMessage) initializer() } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagBuilderExt.kt index 226d110ecc..4562d52754 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagBuilderExt.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.DurationTag import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.HashSha256Tag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.MimeTypeTag import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.WaveformTag /** @@ -34,3 +35,5 @@ fun IMetaTagBuilder.hash(hash: HexKey) = add(HashSha256Tag.TAG_NAME, hash) fun IMetaTagBuilder.duration(size: Int) = add(DurationTag.TAG_NAME, size.toString()) fun IMetaTagBuilder.waveform(wave: List) = add(WaveformTag.TAG_NAME, WaveformTag.assembleWave(wave)) + +fun IMetaTagBuilder.mimeType(mime: String) = add(MimeTypeTag.TAG_NAME, mime) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagExt.kt index d65ab34e96..370f830f08 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/IMetaTagExt.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nipA0VoiceMessages import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.DurationTag import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.HashSha256Tag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.MimeTypeTag import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.WaveformTag fun IMetaTag.hash() = properties.get(HashSha256Tag.TAG_NAME) @@ -30,3 +31,5 @@ fun IMetaTag.hash() = properties.get(HashSha256Tag.TAG_NAME) fun IMetaTag.duration() = properties.get(DurationTag.TAG_NAME) fun IMetaTag.waveform() = properties.get(WaveformTag.TAG_NAME) + +fun IMetaTag.mimeType() = properties.get(MimeTypeTag.TAG_NAME) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/TagArrayBuilderExt.kt index 1854a71bf1..f482eb2019 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/TagArrayBuilderExt.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/TagArrayBuilderExt.kt @@ -20,15 +20,21 @@ */ package com.vitorpamplona.quartz.nipA0VoiceMessages +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashSha256Tag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyAuthorTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyEventTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyKindTag fun TagArrayBuilder.audioIMeta( url: String, + mimeType: String? = null, hash: String? = null, duration: Int? = null, waveform: List? = null, -) = audioIMeta(AudioMeta(url, hash, duration, waveform)) +) = audioIMeta(AudioMeta(url, mimeType, hash, duration, waveform)) fun TagArrayBuilder.audioIMeta(imeta: AudioMeta): TagArrayBuilder { add(imeta.toIMetaArray()) @@ -40,3 +46,18 @@ fun TagArrayBuilder.audioIMeta(audioUrls: List.replyEvent( + eventId: String, + relayHint: NormalizedRelayUrl?, + pubkey: String?, +) = addUnique(ReplyEventTag.assemble(eventId, relayHint, pubkey)) + +fun TagArrayBuilder.replyKind(kind: String) = addUnique(ReplyKindTag.assemble(kind)) + +fun TagArrayBuilder.replyKind(kind: Int) = addUnique(ReplyKindTag.assemble(kind)) + +fun TagArrayBuilder.replyAuthor( + pubKey: HexKey, + relay: NormalizedRelayUrl?, +) = add(ReplyAuthorTag.assemble(pubKey, relay)) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceEvent.kt index a43185712b..33f30d596f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceEvent.kt @@ -38,6 +38,14 @@ class VoiceEvent( const val KIND = 1222 const val ALT_DESCRIPTION = "Voice message" + fun build( + url: String, + mimeType: String?, + hash: String, + duration: Int, + waveform: List, + ) = build(AudioMeta(url, mimeType, hash, duration, waveform)) + fun build( voiceMessage: AudioMeta, createdAt: Long = TimeUtils.now(), diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceReplyEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceReplyEvent.kt index 5d43098cd2..eb7444881d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceReplyEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/VoiceReplyEvent.kt @@ -23,7 +23,12 @@ package com.vitorpamplona.quartz.nipA0VoiceMessages import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyAuthorTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyEventTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyKindTag import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull @Immutable class VoiceReplyEvent( @@ -34,14 +39,45 @@ class VoiceReplyEvent( content: String, sig: HexKey, ) : BaseVoiceEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun replyAuthor() = tags.firstNotNullOfOrNull(ReplyAuthorTag::parse) + + fun replyAuthors() = tags.filter(ReplyAuthorTag::match) + + fun replyAuthorKeys() = tags.mapNotNull(ReplyAuthorTag::parseKey) + + fun replyAuthorHints() = tags.mapNotNull(ReplyAuthorTag::parseAsHint) + + fun directReplies() = tags.filter { ReplyEventTag.match(it) } + + fun directKinds() = tags.filter(ReplyKindTag::match) + + fun markedReplyTos(): List = tags.mapNotNull(ReplyEventTag::parseKey) + + fun replyingTo(): HexKey? = tags.lastNotNullOfOrNull(ReplyEventTag::parseKey) + companion object { const val KIND = 1244 const val ALT_DESCRIPTION = "Voice reply" + fun build( + url: String, + mimeType: String?, + hash: String, + duration: Int, + waveform: List, + replyingTo: EventHintBundle, + ) = build(AudioMeta(url, mimeType, hash, duration, waveform), replyingTo) + fun build( voiceMessage: AudioMeta, + replyingTo: EventHintBundle, createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, - ) = build(voiceMessage, KIND, ALT_DESCRIPTION, createdAt, initializer) + ) = build(voiceMessage, KIND, ALT_DESCRIPTION, createdAt) { + replyEvent(replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey) + replyKind(replyingTo.event.kind) + replyAuthor(replyingTo.event.pubKey, replyingTo.authorHomeRelay) + initializer() + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/MimeTypeTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/MimeTypeTag.kt new file mode 100644 index 0000000000..2dc30e0397 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/MimeTypeTag.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +class MimeTypeTag { + companion object { + const val TAG_NAME = "m" + + fun isIn( + tag: Array, + mimeTypes: Set, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in mimeTypes + + @JvmStatic + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + @JvmStatic + fun assemble(mimeType: String) = arrayOf(TAG_NAME, mimeType) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyAuthorTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyAuthorTag.kt new file mode 100644 index 0000000000..5853f5f7ef --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyAuthorTag.kt @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.ensure + +@Immutable +data class ReplyAuthorTag( + override val pubKey: HexKey, + override val relayHint: NormalizedRelayUrl? = null, +) : PubKeyReferenceTag { + fun toTagArray() = assemble(pubKey, relayHint) + + companion object { + const val TAG_NAME = "p" + + @JvmStatic + fun match(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun parse(tag: Tag): ReplyAuthorTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + + val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } + + return ReplyAuthorTag(tag[1], hint) + } + + @JvmStatic + fun parseKey(tag: Tag): HexKey? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + return tag[1] + } + + @JvmStatic + fun parseAsHint(tag: Array): PubKeyHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(hint != null) { return null } + + return PubKeyHint(tag[1], hint) + } + + @JvmStatic + fun assemble( + pubkey: HexKey, + relayHint: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, pubkey, relayHint?.url) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyEventTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyEventTag.kt new file mode 100644 index 0000000000..08c10fd9fa --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyEventTag.kt @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages.tags + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip01Core.tags.events.EventReference +import com.vitorpamplona.quartz.utils.Hex +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.ensure + +@Immutable +class ReplyEventTag( + val ref: EventReference, +) { + constructor(eventId: String, relayHint: NormalizedRelayUrl?, pubkey: String?) : this(EventReference(eventId, pubkey, relayHint)) + + fun toTagArray() = assemble(ref) + + companion object { + const val TAG_NAME = "e" + + @JvmStatic + fun match(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun isTagged( + tag: Array, + eventId: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == eventId + + @JvmStatic + fun isIn( + tag: Array, + eventIds: Set, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in eventIds + + @JvmStatic + fun parse(tag: Array): ReplyEventTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + return ReplyEventTag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, tag.getOrNull(3)) + } + + @JvmStatic + fun parseKey(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + return tag[1] + } + + @JvmStatic + fun parseValidKey(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(Hex.isHex(tag[1])) { return null } + return tag[1] + } + + @JvmStatic + fun parseAsHint(tag: Array): EventIdHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].length == 64) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return EventIdHint(tag[1], relayHint) + } + + @JvmStatic + fun assemble( + eventId: HexKey, + relay: NormalizedRelayUrl?, + pubkey: String?, + ) = arrayOfNotNull(TAG_NAME, eventId, relay?.url, pubkey) + + @JvmStatic + fun assemble(ref: EventReference) = assemble(ref.eventId, ref.relayHint, ref.author) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyKindTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyKindTag.kt new file mode 100644 index 0000000000..7ecb255b7f --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nipA0VoiceMessages/tags/ReplyKindTag.kt @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nipA0VoiceMessages.tags + +import com.vitorpamplona.quartz.nip01Core.core.Tag +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId +import com.vitorpamplona.quartz.utils.ensure + +class ReplyKindTag { + companion object { + const val TAG_NAME = "k" + + @JvmStatic + fun match(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + @JvmStatic + fun isKind( + tag: Tag, + kind: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == kind + + @JvmStatic + fun isTagged( + tag: Tag, + kind: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == kind + + @JvmStatic + fun isIn( + tag: Tag, + kinds: Set, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in kinds + + @JvmStatic + fun parse(tag: Tag): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + @JvmStatic + fun assemble(kind: String) = arrayOf(TAG_NAME, kind) + + @JvmStatic + fun assemble(kind: Int) = arrayOf(TAG_NAME, kind.toString()) + + @JvmStatic + fun assemble(id: ExternalId) = assemble(id.toKind()) + + @JvmStatic + fun assemble(kinds: List): List = kinds.map { assemble(it) } + + @JvmStatic + fun assemble(kinds: Set): List = kinds.map { assemble(it) } + } +} From 2c6a08480826e7f2d2f94ee7bc878e8154209344 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 10:37:30 -0400 Subject: [PATCH 154/185] Makes sure the NostrClient stays off in the background --- .../nip01Core/relay/client/NostrClient.kt | 101 +++++++++++------- .../client/single/basic/BasicRelayClient.kt | 2 +- 2 files changed, 61 insertions(+), 42 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index e2b587d9b1..1b629750c4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.NonCancellable.isActive import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn @@ -80,6 +81,11 @@ class NostrClient( private var listeners = setOf() + // controls the state of the client in such a way that if it is active + // new filters will be sent to the relays and a potential reconnect can + // be triggered. + private var isActive = false + /** * Whatches for any changes in the relay list from subscriptions or outbox * and updates the relayPool as needed. @@ -117,10 +123,15 @@ class NostrClient( fun allAvailableRelays() = relayPool.getAll() // Reconnects all relays that may have disconnected - fun connect() = relayPool.connect() + fun connect() { + isActive = true + relayPool.connect() + } - // Reconnects all relays that may have disconnected - fun disconnect() = relayPool.disconnect() + fun disconnect() { + isActive = false + relayPool.disconnect() + } @Synchronized fun reconnect(onlyIfChanged: Boolean = false) { @@ -200,26 +211,28 @@ class NostrClient( val oldFilters = activeRequests.getSubscriptionFiltersOrNull(subId) ?: emptyMap() activeRequests.addOrUpdate(subId, filters) - val allRelays = filters.keys + oldFilters.keys + if (isActive) { + val allRelays = filters.keys + oldFilters.keys - allRelays.forEach { relay -> - val oldFilters = oldFilters[relay] - val newFilters = filters[relay] + allRelays.forEach { relay -> + val oldFilters = oldFilters[relay] + val newFilters = filters[relay] - if (newFilters.isNullOrEmpty()) { - // some relays are not in this sub anymore. Stop their subscriptions - relayPool.close(relay, subId) - } else if (oldFilters.isNullOrEmpty()) { - // new relays were added. Start a new sub in them - relayPool.sendRequest(relay, subId, newFilters) - } else if (needsToResendRequest(oldFilters, newFilters)) { - // filters were changed enough (not only an update in since) to warn a new update - relayPool.sendRequest(relay, subId, newFilters) - } else { - // makes sure the relay wakes up if it was disconnected by the server - // upon connection, the relay will run the default Sync and update all - // filters, including this one. - relayPool.connectIfDisconnected(relay) + if (newFilters.isNullOrEmpty()) { + // some relays are not in this sub anymore. Stop their subscriptions + relayPool.close(relay, subId) + } else if (oldFilters.isNullOrEmpty()) { + // new relays were added. Start a new sub in them + relayPool.sendRequest(relay, subId, newFilters) + } else if (needsToResendRequest(oldFilters, newFilters)) { + // filters were changed enough (not only an update in since) to warn a new update + relayPool.sendRequest(relay, subId, newFilters) + } else { + // makes sure the relay wakes up if it was disconnected by the server + // upon connection, the relay will run the default Sync and update all + // filters, including this one. + relayPool.connectIfDisconnected(relay) + } } } } @@ -231,26 +244,28 @@ class NostrClient( val oldFilters = activeCounts.getSubscriptionFiltersOrNull(subId) ?: emptyMap() activeCounts.addOrUpdate(subId, filters) - val allRelays = filters.keys + oldFilters.keys + if (isActive) { + val allRelays = filters.keys + oldFilters.keys - allRelays.forEach { relay -> - val oldFilters = oldFilters[relay] - val newFilters = filters[relay] + allRelays.forEach { relay -> + val oldFilters = oldFilters[relay] + val newFilters = filters[relay] - if (newFilters.isNullOrEmpty()) { - // some relays are not in this sub anymore. Stop their subscriptions - relayPool.close(relay, subId) - } else if (oldFilters.isNullOrEmpty()) { - // new relays were added. Start a new sub in them - relayPool.sendCount(relay, subId, newFilters) - } else if (needsToResendRequest(oldFilters, newFilters)) { - // filters were changed enough (not only an update in since) to warn a new update - relayPool.sendCount(relay, subId, newFilters) - } else { - // makes sure the relay wakes up if it was disconnected by the server - // upon connection, the relay will run the default Sync and update all - // filters, including this one. - relayPool.connectIfDisconnected(relay) + if (newFilters.isNullOrEmpty()) { + // some relays are not in this sub anymore. Stop their subscriptions + relayPool.close(relay, subId) + } else if (oldFilters.isNullOrEmpty()) { + // new relays were added. Start a new sub in them + relayPool.sendCount(relay, subId, newFilters) + } else if (needsToResendRequest(oldFilters, newFilters)) { + // filters were changed enough (not only an update in since) to warn a new update + relayPool.sendCount(relay, subId, newFilters) + } else { + // makes sure the relay wakes up if it was disconnected by the server + // upon connection, the relay will run the default Sync and update all + // filters, including this one. + relayPool.connectIfDisconnected(relay) + } } } } @@ -259,7 +274,9 @@ class NostrClient( event: Event, connectedRelay: NormalizedRelayUrl, ) { - relayPool.getRelay(connectedRelay)?.send(event) + if (isActive) { + relayPool.getRelay(connectedRelay)?.send(event) + } } fun send( @@ -267,7 +284,9 @@ class NostrClient( relayList: Set, ) { eventOutbox.markAsSending(event, relayList) - relayPool.send(event, relayList) + if (isActive) { + relayPool.send(event, relayList) + } } fun close(subscriptionId: String) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 2a5803871a..031362f8b8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -442,7 +442,7 @@ open class BasicRelayClient( override fun close(subscriptionId: String) { // avoids sending closes for subscriptions that were never sent to this relay. if (afterEOSEPerSubscription.containsKey(subscriptionId)) { - writeToSocket(CloseCmd.Companion.toJson(subscriptionId)) + writeToSocket(CloseCmd.toJson(subscriptionId)) afterEOSEPerSubscription[subscriptionId] = false } } From fa53ff2c7801b6af13270eda1684f87b835fd670 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 12:47:31 -0400 Subject: [PATCH 155/185] Adds an iMeta cache for Picture and Video kinds and speeds up the procedure to verify if a video or picture is supported. --- .../loggedIn/video/dal/SupportedContent.kt | 46 ++++++++++++ .../loggedIn/video/dal/VideoFeedFilter.kt | 36 +++------ .../video/dal/SupportedContentTest.kt | 56 ++++++++++++++ .../commons/richtext/RichTextParser.kt | 6 +- .../quartz/nip68Picture/PictureEvent.kt | 73 +++---------------- .../quartz/nip68Picture/tags/LocationTag.kt | 41 +++++++++++ .../quartz/nip71Video/VideoEvent.kt | 72 ++---------------- .../quartz/nip92IMeta/IMetaTag.kt | 2 +- 8 files changed, 176 insertions(+), 156 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContent.kt create mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContentTest.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/tags/LocationTag.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContent.kt new file mode 100644 index 0000000000..cd41fb2005 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContent.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal + +class SupportedContent( + val blockedUrls: List, + val mimeTypes: Set, + val supportedFileExtensions: Set, +) { + private fun validExtension(fullUrl: String): Boolean { + val queryIndex = fullUrl.indexOf('?') + if (queryIndex > 0) { + return supportedFileExtensions.any { fullUrl.startsWith(it, queryIndex - it.length) } + } + + val fragmentIndex = fullUrl.indexOf('#') + if (fragmentIndex > 0) { + return supportedFileExtensions.any { fullUrl.startsWith(it, fragmentIndex - it.length) } + } + + return supportedFileExtensions.any { fullUrl.endsWith(it) } + } + + fun acceptableUrl( + url: String, + mimeType: String?, + ) = blockedUrls.none { url.contains(it) } && ((mimeType != null && mimeTypes.contains(mimeType)) || validExtension(url)) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt index ea2fb6d629..b0744b1230 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/VideoFeedFilter.kt @@ -43,6 +43,13 @@ import com.vitorpamplona.quartz.nip94FileMetadata.FileHeaderEvent class VideoFeedFilter( val account: Account, ) : AdditiveFeedFilter() { + val videoFeedSupport = + SupportedContent( + blockedUrls = listOf("youtu.be", "youtube.com"), + mimeTypes = SUPPORTED_VIDEO_FEED_MIME_TYPES_SET, + supportedFileExtensions = (RichTextParser.videoExtensions + RichTextParser.imageExtensions).toSet(), + ) + override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultStoriesFollowList.value override fun limit() = 300 @@ -76,24 +83,11 @@ class VideoFeedFilter( fun acceptableUrls( baseUrls: List, mimeType: String?, - ): Boolean { - // we don't have an youtube player - val urls = baseUrls.filter { !(it.contains("youtu.be") || it.contains("youtube.com")) } + ) = baseUrls.any { videoFeedSupport.acceptableUrl(it, mimeType) } - val isSupportedMimeType = mimeType?.let { SUPPORTED_VIDEO_FEED_MIME_TYPES_SET.contains(it) } == true + fun acceptableVideoiMetas(iMetas: List): Boolean = iMetas.any { videoFeedSupport.acceptableUrl(it.url, it.mimeType) } - return urls.isNotEmpty() && (urls.any { RichTextParser.Companion.isImageOrVideoUrl(it) } || isSupportedMimeType) - } - - fun acceptableVideoiMetas(iMetas: List): Boolean = - iMetas.any { - !it.url.contains("youtu.be") && !it.url.contains("youtube.com") && (RichTextParser.isImageOrVideoUrl(it.url) || (it.mimeType == null || SUPPORTED_VIDEO_FEED_MIME_TYPES_SET.contains(it.mimeType))) - } - - fun acceptablePictureiMetas(iMetas: List): Boolean = - iMetas.any { - !it.url.contains("youtu.be") && !it.url.contains("youtube.com") && (RichTextParser.isImageOrVideoUrl(it.url) || (it.mimeType == null || SUPPORTED_VIDEO_FEED_MIME_TYPES_SET.contains(it.mimeType))) - } + fun acceptablePictureiMetas(iMetas: List): Boolean = iMetas.any { videoFeedSupport.acceptableUrl(it.url, it.mimeType) } fun acceptanceEvent(noteEvent: PictureEvent) = acceptablePictureiMetas(noteEvent.imetaTags()) @@ -117,14 +111,8 @@ class VideoFeedFilter( (noteEvent is FileHeaderEvent && acceptanceEvent(noteEvent)) || (noteEvent is VideoVerticalEvent && acceptanceEvent(noteEvent)) || (noteEvent is VideoHorizontalEvent && acceptanceEvent(noteEvent)) || - ( - noteEvent is FileStorageHeaderEvent && - noteEvent.isOneOf( - SUPPORTED_VIDEO_FEED_MIME_TYPES_SET, - ) - ) || - noteEvent is PictureEvent && - acceptanceEvent(noteEvent) + (noteEvent is FileStorageHeaderEvent && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) || + (noteEvent is PictureEvent && acceptanceEvent(noteEvent)) ) && params.match(noteEvent) && (params.isHiddenList || account.isAcceptable(note)) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContentTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContentTest.kt new file mode 100644 index 0000000000..3219cde5ba --- /dev/null +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/dal/SupportedContentTest.kt @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.video.dal + +import junit.framework.TestCase.assertTrue +import org.junit.Test + +class SupportedContentTest { + @Test + fun acceptableUrl() { + val supportedExtensions = setOf(".mp4", ".webm", ".jpg", ".png") + val mimeTypes = setOf("video/mp4", "video/webm", "image/jpeg", "image/png") + val blockedUrls = listOf("youtube.com", "youtu.be") + + val testVideoUrl = "https://example.com/video.mp4" + val blockedUrl = "https://youtube.com/watch?v=example" + val urlWithQuery = "https://example.com/media.jpg?param=1" + val urlWithFragment = "https://example.com/data.png#section" + + val contentSupport = SupportedContent(blockedUrls, mimeTypes, supportedExtensions) + + // Valid scenarios + assertTrue(contentSupport.acceptableUrl(testVideoUrl, "video/mp4")) // Valid extension and mime + assertTrue(contentSupport.acceptableUrl(urlWithQuery, "image/jpeg")) // Valid query param extension + assertTrue(contentSupport.acceptableUrl(urlWithFragment, "image/png")) // Valid fragment extension + + assertTrue(contentSupport.acceptableUrl(testVideoUrl, null)) + assertTrue(contentSupport.acceptableUrl(urlWithQuery, null)) + assertTrue(contentSupport.acceptableUrl(urlWithFragment, null)) + + // Blocked URL scenarios + assertTrue(!contentSupport.acceptableUrl(blockedUrl, null)) // Blocked URL + + // Invalid scenarios + assertTrue(!contentSupport.acceptableUrl("https://example.com/file.docx", "application/docx")) // Unsupported extension/mime + assertTrue(!contentSupport.acceptableUrl("https://example.com/file.docx", null)) // Unsupported extension/mime + } +} diff --git a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index 8306893980..6d17f3189e 100644 --- a/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/main/java/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -367,11 +367,11 @@ class RichTextParser { private fun removeQueryParamsForExtensionComparison(fullUrl: String): String = if (fullUrl.contains("?")) { - fullUrl.split("?")[0].lowercase() + fullUrl.split("?")[0] } else if (fullUrl.contains("#")) { - fullUrl.split("#")[0].lowercase() + fullUrl.split("#")[0] } else { - fullUrl.lowercase() + fullUrl } fun isImageOrVideoUrl(url: String): Boolean { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt index bb0bca02ad..7315ecf396 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/PictureEvent.kt @@ -24,25 +24,16 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohashes +import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip68Picture.tags.LocationTag import com.vitorpamplona.quartz.nip92IMeta.imetas -import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashSha256Tag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash -import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag -import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -55,63 +46,21 @@ class PictureEvent( sig: HexKey, ) : Event(id, pubKey, createdAt, KIND, tags, content, sig), RootScope { - // --------------- - // current - // -------------- + @Transient var iMetas: List? = null fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - /** old standard didnt use IMetas **/ - private fun url() = tags.firstNotNullOfOrNull(UrlTag::parse) + fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - private fun urls() = tags.mapNotNull(UrlTag::parse) + fun hash() = tags.firstNotNullOfOrNull(HashSha256Tag::parse) - private fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) + fun hashtags() = tags.hashtags() - private fun hash() = tags.firstNotNullOfOrNull(HashSha256Tag::parse) + fun geohashes() = tags.geohashes() - private fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) + fun location() = tags.mapNotNull(LocationTag::parse) - private fun dimensions() = tags.firstNotNullOfOrNull(DimensionTag::parse) - - private fun magnetURI() = tags.firstNotNullOfOrNull(MagnetTag::parse) - - private fun torrentInfoHash() = tags.firstNotNullOfOrNull(TorrentInfoHash::parse) - - private fun blurhash() = tags.firstNotNullOfOrNull(BlurhashTag::parse) - - private fun hasUrl() = tags.any(UrlTag::isTag) - - private fun isOneOf(mimeTypes: Set) = tags.any(MimeTypeTag::isIn, mimeTypes) - - private fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - - private fun images() = tags.mapNotNull(ImageTag::parse) - - private fun thumb() = tags.firstNotNullOfOrNull(ThumbTag::parse) - - private fun service() = tags.firstNotNullOfOrNull(ServiceTag::parse) - - private fun fallbacks() = tags.mapNotNull(FallbackTag::parse) - - // hack to fix pablo's bug - fun rootImage() = - url()?.let { - PictureMeta( - url = it, - mimeType = mimeType(), - blurhash = blurhash(), - alt = alt(), - hash = hash(), - dimension = dimensions(), - size = size(), - service = service(), - fallback = fallbacks(), - annotations = emptyList(), - ) - } - - fun imetaTags() = imetas().map { PictureMeta.parse(it) }.plus(rootImage()).filterNotNull() + fun imetaTags() = iMetas ?: imetas().map { PictureMeta.parse(it) }.also { iMetas = it } companion object { const val KIND = 20 @@ -142,7 +91,7 @@ class PictureEvent( createdAt: Long = TimeUtils.now(), initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, description, createdAt) { - alt(ClassifiedsEvent.ALT_DESCRIPTION) + alt(ALT_DESCRIPTION) initializer() } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/tags/LocationTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/tags/LocationTag.kt new file mode 100644 index 0000000000..a46e0e2ee1 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip68Picture/tags/LocationTag.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip68Picture.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +class LocationTag { + companion object { + const val TAG_NAME = "location" + + @JvmStatic + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + @JvmStatic + fun assemble(locationName: String) = arrayOf(TAG_NAME, locationName) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt index 0edd44b83a..2d2b61cc9f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip71Video/VideoEvent.kt @@ -23,29 +23,17 @@ package com.vitorpamplona.quartz.nip71Video import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.core.any import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag -import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip71Video.tags.DurationTag import com.vitorpamplona.quartz.nip71Video.tags.SegmentTag import com.vitorpamplona.quartz.nip92IMeta.imetas -import com.vitorpamplona.quartz.nip94FileMetadata.tags.BlurhashTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.FallbackTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashSha256Tag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.ImageTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.MagnetTag import com.vitorpamplona.quartz.nip94FileMetadata.tags.MimeTypeTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.ServiceTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.SizeTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.ThumbTag -import com.vitorpamplona.quartz.nip94FileMetadata.tags.TorrentInfoHash -import com.vitorpamplona.quartz.nip94FileMetadata.tags.UrlTag @Immutable abstract class VideoEvent( @@ -58,59 +46,7 @@ abstract class VideoEvent( sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig), RootScope { - /** old standard didnt use IMetas **/ - private fun url() = tags.firstNotNullOfOrNull(UrlTag::parse) - - private fun urls() = tags.mapNotNull(UrlTag::parse) - - private fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) - - private fun hash() = tags.firstNotNullOfOrNull(HashSha256Tag::parse) - - private fun size() = tags.firstNotNullOfOrNull(SizeTag::parse) - - private fun dimensions() = tags.firstNotNullOfOrNull(DimensionTag::parse) - - private fun magnetURI() = tags.firstNotNullOfOrNull(MagnetTag::parse) - - private fun torrentInfoHash() = tags.firstNotNullOfOrNull(TorrentInfoHash::parse) - - private fun blurhash() = tags.firstNotNullOfOrNull(BlurhashTag::parse) - - private fun hasUrl() = tags.any(UrlTag::isTag) - - private fun isOneOf(mimeTypes: Set) = tags.any(MimeTypeTag::isIn, mimeTypes) - - private fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - - private fun images() = tags.mapNotNull(ImageTag::parse) - - private fun thumb() = tags.firstNotNullOfOrNull(ThumbTag::parse) - - private fun service() = tags.firstNotNullOfOrNull(ServiceTag::parse) - - private fun fallbacks() = tags.mapNotNull(FallbackTag::parse) - - // hack to fix pablo's bug - fun rootVideo() = - url()?.let { - VideoMeta( - url = it, - mimeType = mimeType(), - blurhash = blurhash(), - alt = alt(), - hash = hash(), - dimension = dimensions(), - size = size(), - service = service(), - fallback = fallbacks(), - image = images().map { it.imageUrl }, - ) - } - - // --------------- - // current - // -------------- + @Transient var iMetas: List? = null fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) @@ -126,5 +62,9 @@ abstract class VideoEvent( fun hashtags() = tags.hashtags() - fun imetaTags() = imetas().map { VideoMeta.parse(it) }.plus(rootVideo()).filterNotNull() + private fun mimeType() = tags.firstNotNullOfOrNull(MimeTypeTag::parse) + + private fun hash() = tags.firstNotNullOfOrNull(HashSha256Tag::parse) + + fun imetaTags() = iMetas ?: imetas().map { VideoMeta.parse(it) }.also { iMetas = it } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTag.kt index 3bcba1c14c..237b11d6bb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip92IMeta/IMetaTag.kt @@ -48,7 +48,7 @@ class IMetaTag( ensure(tag[1].isNotEmpty()) { return null } val allTags = parseIMeta(tag) - val url = allTags.get(ANCHOR_PROPERTY)?.firstOrNull() + val url = allTags[ANCHOR_PROPERTY]?.firstOrNull() return if (url != null) { IMetaTag(url, allTags.minus(ANCHOR_PROPERTY)) From 414e933686055e53ab01fc0e48fc6e55279d01f3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 16:49:48 -0400 Subject: [PATCH 156/185] Only trigger new filters if this has changed --- .../com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt | 5 ++++- .../ui/screen/loggedIn/notifications/CardFeedContentState.kt | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt index 33e36bdd3c..a01ee756bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt @@ -108,7 +108,10 @@ class FeedContentState( private fun updateFeed(notes: ImmutableList) { if (notes.size >= localFilter.limit()) { - lastNoteCreatedAtWhenFullyLoaded.tryEmit(notes.lastOrNull { it.event != null }?.createdAt()) + val lastNomeTime = notes.lastOrNull { it.event != null }?.createdAt() + if (lastNomeTime != lastNoteCreatedAtWhenFullyLoaded.value) { + lastNoteCreatedAtWhenFullyLoaded.tryEmit(lastNomeTime) + } } val currentState = _feedContent.value diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index 95df60a549..59e3e78011 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -305,7 +305,10 @@ class CardFeedContentState( private fun updateFeed(notes: ImmutableList) { if (notes.size >= localFilter.limit()) { - lastNoteCreatedAtWhenFullyLoaded.tryEmit(notes.lastOrNull()?.createdAt()) + val lastNomeTime = notes.lastOrNull()?.createdAt() + if (lastNomeTime != lastNoteCreatedAtWhenFullyLoaded.value) { + lastNoteCreatedAtWhenFullyLoaded.tryEmit(notes.lastOrNull()?.createdAt()) + } } val currentState = _feedContent.value From cf1d66bc01169410eae217e2582adbd9a34c0fd9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 16:50:36 -0400 Subject: [PATCH 157/185] new parameter to only log when a given amount of ms have passed. --- .../src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt | 5 ++++- .../amethyst/service/okhttp/OkHttpClientFactory.kt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 6fd11fd166..eafafe292d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -152,11 +152,14 @@ fun debugState(context: Context) { inline fun logTime( debugMessage: String, + minToReportMs: Int = 0, block: () -> T, ): T = if (isDebug) { val (result, elapsed) = measureTimedValue(block) - Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage") + if (elapsed.inWholeMilliseconds > minToReportMs) { + Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: $debugMessage") + } result } else { block() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index 16918a4a96..bf87bdbcbb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -30,7 +30,7 @@ class OkHttpClientFactory( keyCache: EncryptionKeyCache, ) { companion object { - // by picking a random proxy port, the connection will fail as it shouold. + // by picking a random proxy port, the connection will fail as it should. const val DEFAULT_SOCKS_PORT: Int = 9050 const val DEFAULT_IS_MOBILE: Boolean = false const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10 From bd7dba1a3137b131418d6cb89b747c7131540b52 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 17:31:24 -0400 Subject: [PATCH 158/185] Fixes more random relay hint errors --- .../relay/normalizer/RelayUrlNormalizer.kt | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index 40a0e51c2d..db469cc285 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -24,6 +24,7 @@ import android.util.Log import androidx.collection.LruCache import kotlinx.coroutines.CancellationException import org.czeal.rfc3986.URIReference +import java.lang.RuntimeException import kotlin.contracts.ExperimentalContracts val normalizedUrls = LruCache(5000) @@ -53,7 +54,14 @@ class RelayUrlNormalizer { fun isHttpSuffix(url: String) = url[4] == ':' && url[5] == '/' && url[6] == '/' fun isRelayUrl(url: String): Boolean { - val trimmed = url.trim().ifEmpty { return false } + if (url.length < 3) return false + + val trimmed = + if (url[0].isWhitespace() || url[url.length - 1].isWhitespace()) { + url.trim() + } else { + url + } // fast if (isRelaySchemePrefix(trimmed)) { @@ -78,9 +86,14 @@ class RelayUrlNormalizer { @OptIn(ExperimentalContracts::class) fun fix(url: String): String? { - val trimmed = url.trim() + if (url.length < 3) return null - if (trimmed.isEmpty()) return null + val trimmed = + if (url[0].isWhitespace() || url[url.length - 1].isWhitespace()) { + url.trim() + } else { + url + } // fast for good wss:// urls if (isRelaySchemePrefix(trimmed)) { @@ -100,12 +113,30 @@ class RelayUrlNormalizer { } } + // fast for good ww:// urls + if (trimmed.startsWith("ww://")) { + return "wss://${trimmed.drop(5)}" + } + + // fast for good ww:// urls + if (trimmed.startsWith("was://")) { + return "wss://${trimmed.drop(6)}" + } + + // fast for good ww:// urls + if (trimmed.startsWith("Wws://")) { + return "wss://${trimmed.drop(6)}" + } + if (trimmed.contains("://")) { // some other scheme we cannot connect to. Log.w("RelayUrlNormalizer", "Rejected relay URL: $url") return null } + Log.w("RelayUrlNormalizer", "Rejected relay URL: $url") + throw RuntimeException("RelayError $trimmed") + return if (isOnion(trimmed) || isLocalHost(trimmed)) { "ws://$trimmed" } else { @@ -114,7 +145,7 @@ class RelayUrlNormalizer { } fun normalize(url: String): NormalizedRelayUrl { - normalizedUrls.get(url)?.let { return it } + normalizedUrls[url]?.let { return it } return try { val fixed = fix(url) ?: return NormalizedRelayUrl(url) @@ -142,6 +173,7 @@ class RelayUrlNormalizer { } } catch (e: Exception) { if (e is CancellationException) throw e + e.printStackTrace() Log.w("NormalizedRelayUrl", "Rejected Error $url") null } From 651ca9a3ef5d31ec98f3dd0f773c59e7c1423ca9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 17:31:38 -0400 Subject: [PATCH 159/185] No need to update status on EOSE --- .../quartz/nip01Core/relay/client/pool/RelayPool.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt index 25eaf2e19a..51b480b5da 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -261,7 +261,6 @@ class RelayPool( arrivalTime: Long, ) { listener.onEOSE(relay, subId, arrivalTime) - updateStatus() } override fun onRelayStateChange( From e70f1fedc68b97a0f2b13b50713e0195ff26165a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 17:33:12 -0400 Subject: [PATCH 160/185] Adds a coroutine to the relay class so that we can defer processing events --- .../com/vitorpamplona/amethyst/Amethyst.kt | 2 +- .../relayClient/CacheClientConnector.kt | 3 + .../nip01Core/relay/client/NostrClient.kt | 1 + .../client/single/basic/BasicRelayClient.kt | 61 +++++++++++++------ .../client/single/simple/SimpleRelayClient.kt | 3 + 5 files changed, 49 insertions(+), 21 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt index 1129c77737..d115ad178d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/Amethyst.kt @@ -112,7 +112,7 @@ class Amethyst : Application() { val relayProxyClientConnector = RelayProxyClientConnector(torProxySettingsAnchor, okHttpClients, connManager, client, applicationIOScope) // Verifies and inserts in the cache from all relays, all subscriptions - val cacheClientConnector = CacheClientConnector(client, cache) + val cacheClientConnector = CacheClientConnector(client, cache, applicationIOScope) // Show messages from the Relay and controls their dismissal val notifyCoordinator = NotifyCoordinator(client) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt index f03417aceb..0371fc4fe0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/CacheClientConnector.kt @@ -21,15 +21,18 @@ package com.vitorpamplona.amethyst.service.relayClient import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.LocalCache.markAsSeen import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.EventCollector import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayInsertConfirmationCollector import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.CoroutineScope class CacheClientConnector( val client: NostrClient, val cache: LocalCache, + val scope: CoroutineScope, ) { val receiver = EventCollector(client) { event, relay -> diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index 1b629750c4..fa5fbf3956 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -114,6 +114,7 @@ class NostrClient( socketBuilder = websocketBuilder, listener = relayPool, stats = RelayStats.get(relay), + scope = scope, ) { liveRelay -> activeRequests.forEachSub(relay, liveRelay::sendRequest) activeCounts.forEachSub(relay, liveRelay::sendCount) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt index 031362f8b8..7555915d93 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/basic/BasicRelayClient.kt @@ -50,6 +50,9 @@ import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.cancellation.CancellationException @@ -76,11 +79,13 @@ open class BasicRelayClient( val socketBuilder: WebsocketBuilder, val listener: IRelayClientListener, val stats: RelayStat = RelayStat(), + val scope: CoroutineScope, val defaultOnConnect: (BasicRelayClient) -> Unit = { }, ) : IRelayClient { companion object { // waits 3 minutes to reconnect once things fail const val DELAY_TO_RECONNECT_IN_MSECS = 500L + const val EVENT_MESSAGE_PREFIX = "[\"${EventMessage.LABEL}\"" } private val logTag = "Relay ${url.displayUrl()}" @@ -160,31 +165,21 @@ open class BasicRelayClient( markConnectionAsReady(pingMillis, compression) - onConnected() + scope.launch(Dispatchers.Default) { + onConnected() + } listener.onRelayStateChange(this@BasicRelayClient, RelayState.CONNECTED) } override fun onMessage(text: String) { - // Log.d(logTag, "Receiving: $text") - stats.addBytesReceived(text.bytesUsedInMemory()) - - try { - when (val msg = parser.parse(text)) { - is EventMessage -> processEvent(msg) - is EoseMessage -> processEose(msg) - is NoticeMessage -> processNotice(msg) - is OkMessage -> processOk(msg, onConnected) - is AuthMessage -> processAuth(msg) - is NotifyMessage -> processNotify(msg) - is ClosedMessage -> processClosed(msg) - else -> processUnkownMessage(text) + if (text.startsWith(EVENT_MESSAGE_PREFIX)) { + // defers the parsing of ["EVENTS" to avoid blocking the HTTP thread + scope.launch(Dispatchers.Default) { + consumeIncomingCommand(text, onConnected) } - } catch (e: Throwable) { - if (e is CancellationException) throw e - stats.newError("Error processing: $text") - Log.e(logTag, "Error processing: $text") - listener.onError(this@BasicRelayClient, "", Error("Error processing $text")) + } else { + consumeIncomingCommand(text, onConnected) } } @@ -231,6 +226,32 @@ open class BasicRelayClient( } } + fun consumeIncomingCommand( + text: String, + onConnected: () -> Unit, + ) { + // Log.d(logTag, "Receiving: $text") + stats.addBytesReceived(text.bytesUsedInMemory()) + + try { + when (val msg = parser.parse(text)) { + is EventMessage -> processEvent(msg) + is EoseMessage -> processEose(msg) + is NoticeMessage -> processNotice(msg) + is OkMessage -> processOk(msg, onConnected) + is AuthMessage -> processAuth(msg) + is NotifyMessage -> processNotify(msg) + is ClosedMessage -> processClosed(msg) + else -> processUnknownMessage(text) + } + } catch (e: Throwable) { + if (e is CancellationException) throw e + stats.newError("Error processing: $text") + Log.e(logTag, "Error processing: $text") + listener.onError(this@BasicRelayClient, "", Error("Error processing $text")) + } + } + fun markConnectionAsReady( pingInMs: Long, usingCompression: Boolean, @@ -315,7 +336,7 @@ open class BasicRelayClient( listener.onClosed(this@BasicRelayClient, msg.subscriptionId, msg.message) } - private fun processUnkownMessage(newMessage: String) { + private fun processUnknownMessage(newMessage: String) { stats.newError("Unsupported message: $newMessage") Log.w(logTag, "Unsupported message: $newMessage") listener.onError(this, "", Error("Unsupported message: $newMessage")) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt index 6064dbc2a8..0619ffb41e 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/single/simple/SimpleRelayClient.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayCl import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStat import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder +import kotlinx.coroutines.CoroutineScope /** * This relay client saves any event that will be sent in an outbox @@ -36,11 +37,13 @@ class SimpleRelayClient( socketBuilder: WebsocketBuilder, listener: IRelayClientListener, stats: RelayStat = RelayStat(), + scopeToParseEvents: CoroutineScope, defaultOnConnect: (BasicRelayClient) -> Unit = { }, ) : IRelayClient by BasicRelayClient( url, socketBuilder, OutboxCache(listener), stats, + scopeToParseEvents, defaultOnConnect, ) From c6e103ff87e19a57b3cbdb672cb68be3988ee849 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 17:33:56 -0400 Subject: [PATCH 161/185] Delete unnecessary logs --- .../quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt index db469cc285..11b8c67b39 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/normalizer/RelayUrlNormalizer.kt @@ -24,7 +24,6 @@ import android.util.Log import androidx.collection.LruCache import kotlinx.coroutines.CancellationException import org.czeal.rfc3986.URIReference -import java.lang.RuntimeException import kotlin.contracts.ExperimentalContracts val normalizedUrls = LruCache(5000) @@ -134,9 +133,6 @@ class RelayUrlNormalizer { return null } - Log.w("RelayUrlNormalizer", "Rejected relay URL: $url") - throw RuntimeException("RelayError $trimmed") - return if (isOnion(trimmed) || isLocalHost(trimmed)) { "ws://$trimmed" } else { @@ -173,7 +169,6 @@ class RelayUrlNormalizer { } } catch (e: Exception) { if (e is CancellationException) throw e - e.printStackTrace() Log.w("NormalizedRelayUrl", "Rejected Error $url") null } From 521a41d81be0519e80c11accaac0870f17fc0a0f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 17:50:21 -0400 Subject: [PATCH 162/185] avoids logging with sub ms times --- .../src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index eafafe292d..7d3d0719b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -152,7 +152,7 @@ fun debugState(context: Context) { inline fun logTime( debugMessage: String, - minToReportMs: Int = 0, + minToReportMs: Int = 1, block: () -> T, ): T = if (isDebug) { @@ -167,11 +167,14 @@ inline fun logTime( inline fun logTime( debugMessage: (T) -> String, + minToReportMs: Int = 1, block: () -> T, ): T = if (isDebug) { val (result, elapsed) = measureTimedValue(block) - Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}") + if (elapsed.inWholeMilliseconds > minToReportMs) { + Log.d("DEBUG-TIME", "${elapsed.toString(DurationUnit.MILLISECONDS, 3).padStart(12)}: ${debugMessage(result)}") + } result } else { block() From 349dfe3d42eff53406923bed8c7029e0f5fe2186 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 18:05:53 -0400 Subject: [PATCH 163/185] Slowdown updates to random relay requests. --- .../AccountNotificationsEoseFromRandomRelaysManager.kt | 4 +++- .../quartz/nip01Core/relay/client/NostrClient.kt | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt index cada0012fa..19ab9b330d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch class AccountNotificationsEoseFromRandomRelaysManager( @@ -61,7 +62,8 @@ class AccountNotificationsEoseFromRandomRelaysManager( userJobMap[user] = listOf( key.account.scope.launch(Dispatchers.Default) { - key.account.followsPerRelay.collectLatest { + // no need to hurry here. we can wait the app stabilize + key.account.followsPerRelay.debounce(5000).collectLatest { invalidateFilters() } }, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index fa5fbf3956..c7dc317b4a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -37,7 +37,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.NonCancellable.isActive import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn From 1ddc9c216b9c2e8417c996d268565bb86e2eccf5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 18:06:24 -0400 Subject: [PATCH 164/185] Marks which relays have been sending unsupported events. --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 03372d5cde..80e089a29f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2844,7 +2844,7 @@ object LocalCache : ILocalCache { is VoiceReplyEvent -> consume(event, relay, wasVerified) is WikiNoteEvent -> consume(event, relay, wasVerified) else -> { - Log.w("Event Not Supported", event.toJson()) + Log.w("Event Not Supported", "From ${relay?.url}: ${event.toJson()}") false } } From 5d4b16641e40aa140841508aa51e353d4b6f8970 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 18:11:11 -0400 Subject: [PATCH 165/185] Fixes count call --- .../quartz/nip01Core/relay/client/pool/RelayPool.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt index 51b480b5da..f745e96b5c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/pool/RelayPool.kt @@ -125,7 +125,7 @@ class RelayPool( subId: String, filters: List, ) { - relays.get(relay)?.sendRequest(subId, filters) + relays.get(relay)?.sendCount(subId, filters) } fun sendCount( From 2e9696745adb5d56a186843d11b6fe637e83581a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 18:23:02 -0400 Subject: [PATCH 166/185] Avoids sending filters on new connections if the client is not active --- .../quartz/nip01Core/relay/client/NostrClient.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt index c7dc317b4a..d741251b7b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/relay/client/NostrClient.kt @@ -115,9 +115,11 @@ class NostrClient( stats = RelayStats.get(relay), scope = scope, ) { liveRelay -> - activeRequests.forEachSub(relay, liveRelay::sendRequest) - activeCounts.forEachSub(relay, liveRelay::sendCount) - eventOutbox.forEachUnsentEvent(relay, liveRelay::send) + if (isActive) { + activeRequests.forEachSub(relay, liveRelay::sendRequest) + activeCounts.forEachSub(relay, liveRelay::sendCount) + eventOutbox.forEachUnsentEvent(relay, liveRelay::send) + } } fun allAvailableRelays() = relayPool.getAll() From a59cd590bcbf36b9d2f79971eda25d4da0e0f681 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 30 Jul 2025 20:20:56 -0400 Subject: [PATCH 167/185] Deletes these very confusing mocks into account viewmodels for the test --- .../loggedIn/home/ShortNotePostViewModel.kt | 12 +- .../ui/actions/NewPostViewModelTest.kt | 132 ------------------ 2 files changed, 7 insertions(+), 137 deletions(-) delete mode 100644 amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModelTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 30d88edf54..5e1366a3b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -281,8 +281,10 @@ open class ShortNotePostViewModel : pTags = null } - canAddInvoice = accountViewModel.userProfile().info?.lnAddress() != null - canAddZapRaiser = accountViewModel.userProfile().info?.lnAddress() != null + val user = account.userProfile() + + canAddInvoice = user.info?.lnAddress() != null + canAddZapRaiser = user.info?.lnAddress() != null canUsePoll = originalNote == null multiOrchestrator = null @@ -290,12 +292,12 @@ open class ShortNotePostViewModel : message = TextFieldValue(message.text + "\nnostr:${quotedNote.toNEvent()}") quotedNote.author?.let { quotedUser -> - if (quotedUser.pubkeyHex != accountViewModel.userProfile().pubkeyHex) { + if (quotedUser.pubkeyHex != user.pubkeyHex) { if (forwardZapTo.value.items.none { it.key.pubkeyHex == quotedUser.pubkeyHex }) { forwardZapTo.value.addItem(quotedUser) } - if (forwardZapTo.value.items.none { it.key.pubkeyHex == accountViewModel.userProfile().pubkeyHex }) { - forwardZapTo.value.addItem(accountViewModel.userProfile()) + if (forwardZapTo.value.items.none { it.key.pubkeyHex == user.pubkeyHex }) { + forwardZapTo.value.addItem(user) } val pos = forwardZapTo.value.items.indexOfFirst { it.key.pubkeyHex == quotedUser.pubkeyHex } diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModelTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModelTest.kt deleted file mode 100644 index 47e6686623..0000000000 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModelTest.kt +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.actions - -import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState.EmojiMedia -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.unmockkAll -import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Test - -@ExperimentalCoroutinesApi -class NewPostViewModelTest { - @MockK - lateinit var accountViewModel: AccountViewModel - - @MockK(relaxed = true) - lateinit var replyingTo: Note - - private lateinit var shortNoteViewModelUnderTest: ShortNotePostViewModel - - @Before - fun setup() { - mockkObject(LocalCache) - every { LocalCache.getOrCreateUser(any()) } returns mockk() - shortNoteViewModelUnderTest = ShortNotePostViewModel() - MockKAnnotations.init(this) - } - - @After - fun tearDown() { - unmockkAll() - } - - @Test - fun `test load with mentions`() = - runTest { - // Arrange: Setup with non empty mentions - every { accountViewModel.account } returns mockk() - - val textNoteEvent = mockk(relaxed = true) - every { textNoteEvent.mentions() } returns listOf(PTag("user1"), PTag("user2")) - every { replyingTo.event } returns textNoteEvent - - every { accountViewModel.userProfile() } returns mockk(relaxed = true) - every { accountViewModel.account.userProfile() } returns mockk(relaxed = true) - every { accountViewModel.account.emoji.myEmojis } returns mockk>>(relaxed = true) - - // Act: Call load with mentions - shortNoteViewModelUnderTest.init(accountViewModel) - shortNoteViewModelUnderTest.load(replyingTo, quote = null, fork = null, version = null, draft = null) - - // Assert - // Two mentions should call LocalCache.getOrCreateUser twice - verify(exactly = 2) { LocalCache.getOrCreateUser(any()) } - } - - @Test - fun `test load with zero mentions`() = - runTest { - every { accountViewModel.account } returns mockk() - - // Arrange: Setup Note with zero mentions - val textNoteEvent = mockk(relaxed = true) - every { textNoteEvent.mentions() } returns emptyList() - every { replyingTo.event } returns textNoteEvent - - every { accountViewModel.userProfile() } returns mockk(relaxed = true) - - // Act: Call load with empty mentions - shortNoteViewModelUnderTest.load(replyingTo, quote = null, fork = null, version = null, draft = null) - - // Assert - // With no mentions LocalCache.getOrCreateUser should not be called - verify(exactly = 0) { LocalCache.getOrCreateUser(any()) } - } - - @Test - fun `test load with empty mentions`() = - runTest { - every { accountViewModel.account } returns mockk() - - // Arrange: Setup empty mentions - val textNoteEvent = mockk(relaxed = true) - every { textNoteEvent.mentions() } returns emptyList() - every { replyingTo.event } returns textNoteEvent - - every { accountViewModel.userProfile() } returns mockk(relaxed = true) - - // Act: Call load with empty mentions - shortNoteViewModelUnderTest.load(replyingTo, quote = null, fork = null, version = null, draft = null) - - // Assert - // Verify LocalCache.getOrCreateUser(it) is not called with empty hex, it will crash the app - verify(exactly = 0) { LocalCache.getOrCreateUser(any()) } - } -} From 504d582d54cfec08c6d2ff557b8890d46c81aec9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 11:31:36 -0400 Subject: [PATCH 168/185] Tuning limits for Global feeds --- .../FilterNotificationsToPubkey.kt | 6 +-- .../datasource/DiscoveryFilterAssembler.kt | 6 +-- ...FollowsSetsAndLiveStreamsSubAssembler2.kt} | 2 +- ...LongFormClassifiedsAndDVMSubAssembler1.kt} | 2 +- ...PublicChatsAndCommunitiesSubAssembler3.kt} | 2 +- .../subassemblies/FilterLongFormGlobal.kt | 3 +- .../subassemblies/FilterPublicChatsGlobal.kt | 42 ++++++++++++------- .../subassemblies/FilterFollowSetsGlobal.kt | 2 +- .../FilterLiveActivitiesGlobal.kt | 35 +++++++++++----- .../subassemblies/FilterCommunitiesGlobal.kt | 5 ++- .../FilterClassifiedsByAuthors.kt | 2 +- .../subassemblies/FilterClassifiedsGlobal.kt | 3 +- .../FilterPictureAndVideoByGeohash.kt | 4 +- .../FilterPictureAndVideoByHashtag.kt | 4 +- .../nip01Core/FilterPictureAndVideoGlobal.kt | 4 +- .../FilterPictureAndVideoByAllCommunities.kt | 8 ++-- 16 files changed, 80 insertions(+), 50 deletions(-) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/{DiscoveryFollowsDiscoverySubAssembler2.kt => DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt} (98%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/{DiscoveryFollowsDiscoverySubAssembler1.kt => DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt} (98%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/{DiscoveryFollowsDiscoverySubAssembler3.kt => DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt} (98%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt index 6da7409fcf..4f75d15218 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/FilterNotificationsToPubkey.kt @@ -95,7 +95,7 @@ fun filterSummaryNotificationsToPubkey( Filter( kinds = SummaryKinds, tags = mapOf("p" to listOf(pubkey)), - limit = if (since == null) 2000 else null, + limit = 2000, since = since, ), ), @@ -116,7 +116,7 @@ fun filterNotificationsToPubkey( Filter( kinds = NotificationsPerKeyKinds, tags = mapOf("p" to listOf(pubkey)), - limit = if (since == null) 50 else null, + limit = 50, since = since, ), ), @@ -126,7 +126,7 @@ fun filterNotificationsToPubkey( Filter( kinds = NotificationsPerKeyKinds2, tags = mapOf("p" to listOf(pubkey)), - limit = if (since == null) 100 else null, + limit = 100, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt index c86d924c80..a8d7a427ba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFilterAssembler.kt @@ -38,9 +38,9 @@ class DiscoveryFilterAssembler( ) : ComposeSubscriptionManager() { val group = listOf( - DiscoveryFollowsDiscoverySubAssembler1(client, ::allKeys), - DiscoveryFollowsDiscoverySubAssembler2(client, ::allKeys), - DiscoveryFollowsDiscoverySubAssembler3(client, ::allKeys), + DiscoveryLongFormClassifiedsAndDVMSubAssembler1(client, ::allKeys), + DiscoveryFollowsSetsAndLiveStreamsSubAssembler2(client, ::allKeys), + DiscoveryPublicChatsAndCommunitiesSubAssembler3(client, ::allKeys), ) override fun invalidateKeys() = invalidateFilters() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt index 2cf987317c..37c2242449 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler2.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsSetsAndLiveStreamsSubAssembler2.kt @@ -36,7 +36,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch -class DiscoveryFollowsDiscoverySubAssembler2( +class DiscoveryFollowsSetsAndLiveStreamsSubAssembler2( client: NostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt index 7c3ef0db6b..00d94583e3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler1.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryLongFormClassifiedsAndDVMSubAssembler1.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch -class DiscoveryFollowsDiscoverySubAssembler1( +class DiscoveryLongFormClassifiedsAndDVMSubAssembler1( client: NostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt index 660896e6c4..ef4fbb0119 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryFollowsDiscoverySubAssembler3.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/datasource/DiscoveryPublicChatsAndCommunitiesSubAssembler3.kt @@ -36,7 +36,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.sample import kotlinx.coroutines.launch -class DiscoveryFollowsDiscoverySubAssembler3( +class DiscoveryPublicChatsAndCommunitiesSubAssembler3( client: NostrClient, allKeys: () -> Set, ) : PerUserAndFollowListEoseManager(client, allKeys) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt index 1c74098a40..4c879bbb32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/subassemblies/FilterLongFormGlobal.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.utils.TimeUtils fun filterLongFormGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -34,7 +35,7 @@ fun filterLongFormGlobal( if (relays.set.isEmpty()) return emptyList() return relays.set.map { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time ?: defaultSince ?: TimeUtils.oneWeekAgo() RelayBasedFilter( relay = it.key, filter = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt index ce424a1c7a..defd9b5022 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/subassemblies/FilterPublicChatsGlobal.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent +import com.vitorpamplona.quartz.utils.TimeUtils fun filterPublicChatsGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -35,21 +36,34 @@ fun filterPublicChatsGlobal( ): List { if (relays.set.isEmpty()) return emptyList() - return relays.set.map { + return relays.set.flatMap { val since = since?.get(it.key)?.time ?: defaultSince - RelayBasedFilter( - relay = it.key, - filter = - Filter( - kinds = - listOf( - ChannelCreateEvent.KIND, - ChannelMetadataEvent.KIND, - ChannelMessageEvent.KIND, - ), - limit = 30, - since = since, - ), + listOf( + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = + listOf( + ChannelCreateEvent.KIND, + ChannelMetadataEvent.KIND, + ), + limit = 30, + since = since, + ), + ), + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = + listOf( + ChannelMessageEvent.KIND, + ), + limit = 50, + since = since ?: TimeUtils.oneWeekAgo(), + ), + ), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt index 7684ecd79f..661e9ca700 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip51FollowSets/subassemblies/FilterFollowSetsGlobal.kt @@ -40,7 +40,7 @@ fun filterFollowSetsGlobal( filter = Filter( kinds = listOf(FollowListEvent.KIND), - limit = 300, + limit = 100, since = since, ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt index d3cb8095d5..8932ad9e23 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip53LiveActivities/subassemblies/FilterLiveActivitiesGlobal.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.utils.TimeUtils fun filterLiveActivitiesGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -34,16 +35,28 @@ fun filterLiveActivitiesGlobal( ): List { if (relays.set.isEmpty()) return emptyList() - return relays.set.map { - val since = since?.get(it.key)?.time ?: defaultSince - RelayBasedFilter( - relay = it.key, - filter = - Filter( - kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND), - limit = 30, - since = since, + return relays.set + .map { + val since = since?.get(it.key)?.time ?: defaultSince + listOf( + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = listOf(LiveActivitiesEvent.KIND), + limit = 30, + since = since ?: TimeUtils.oneWeekAgo(), + ), ), - ) - } + RelayBasedFilter( + relay = it.key, + filter = + Filter( + kinds = listOf(LiveActivitiesChatMessageEvent.KIND), + limit = 50, + since = since ?: TimeUtils.oneDayAgo(), + ), + ), + ) + }.flatten() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt index a9baaba88c..6798e81f6b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip72Communities/subassemblies/FilterCommunitiesGlobal.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.TimeUtils fun filterCommunitiesGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -51,8 +52,8 @@ fun filterCommunitiesGlobal( filter = Filter( kinds = listOf(CommunityPostApprovalEvent.KIND), - limit = 30, - since = since, + limit = 100, + since = since ?: TimeUtils.oneWeekAgo(), ), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt index 0b999913a9..de27c901a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsByAuthors.kt @@ -42,7 +42,7 @@ fun filterClassifiedsAuthors( Filter( authors = authorList, kinds = listOf(ClassifiedsEvent.KIND), - limit = 300, + limit = authorList.size * 10, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt index 993e2a118e..f91c5cde5c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/subassemblies/FilterClassifiedsGlobal.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import com.vitorpamplona.quartz.utils.TimeUtils fun filterClassifiedsGlobal( relays: GlobalTopNavPerRelayFilterSet, @@ -34,7 +35,7 @@ fun filterClassifiedsGlobal( if (relays.set.isEmpty()) return emptyList() return relays.set.flatMap { - val since = since?.get(it.key)?.time ?: defaultSince + val since = since?.get(it.key)?.time ?: defaultSince ?: TimeUtils.oneMonthAgo() listOf( RelayBasedFilter( relay = it.key, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt index e8075c4dab..5cbc0db864 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByGeohash.kt @@ -43,7 +43,7 @@ fun filterPictureAndVideoGeohash( Filter( kinds = PictureAndVideoKinds, tags = mapOf("g" to geoHashes), - limit = if (since == null) 200 else null, + limit = 200, since = since, ), ), @@ -57,7 +57,7 @@ fun filterPictureAndVideoGeohash( "g" to geoHashes, "m" to LegacyMimeTypes, ), - limit = if (since == null) 200 else null, + limit = 200, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt index 797cb856fb..1b7bff4be4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoByHashtag.kt @@ -44,7 +44,7 @@ fun filterPictureAndVideoHashtag( Filter( kinds = PictureAndVideoKinds, tags = mapOf("t" to hashtags), - limit = if (since == null) 100 else null, + limit = 100, since = since, ), ), @@ -58,7 +58,7 @@ fun filterPictureAndVideoHashtag( "t" to hashtags, "m" to LegacyMimeTypes, ), - limit = if (since == null) 100 else null, + limit = 100, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt index 2e2295ea5c..a5c03b7ee6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip01Core/FilterPictureAndVideoGlobal.kt @@ -43,7 +43,7 @@ fun filterPictureAndVideoGlobal( filter = Filter( kinds = PictureAndVideoKinds, - limit = if (since == null) 50 else null, + limit = 50, since = since, ), ), @@ -53,7 +53,7 @@ fun filterPictureAndVideoGlobal( Filter( kinds = PictureAndVideoLegacyKinds, tags = LegacyMimeTypeMap, - limit = if (since == null) 50 else null, + limit = 50, since = since, ), ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt index 90d0bafae5..b7be4a067f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/datasource/subassemblies/nip72Communities/FilterPictureAndVideoByAllCommunities.kt @@ -52,7 +52,7 @@ fun filterPictureAndVideoAllCommunities( "a" to communityList, "k" to PictureAndVideoKTags, ), - limit = if (since == null) 200 else null, + limit = 200, since = since, ), ), @@ -63,7 +63,7 @@ fun filterPictureAndVideoAllCommunities( Filter( tags = mapOf("a" to communityList), kinds = PictureAndVideoKinds, - limit = if (since == null) 200 else null, + limit = 200, since = since, ), ), @@ -78,7 +78,7 @@ fun filterPictureAndVideoAllCommunities( "a" to communityList, "k" to listOf(FileHeaderEvent.KIND.toString(), FileStorageHeaderEvent.KIND.toString()), ), - limit = if (since == null) 200 else null, + limit = 200, since = since, ), ), @@ -93,7 +93,7 @@ fun filterPictureAndVideoAllCommunities( "m" to LegacyMimeTypes, ), kinds = PictureAndVideoLegacyKinds, - limit = if (since == null) 200 else null, + limit = 200, since = since, ), ), From 12f793ad517f99a9469080e94dbef1e180805c03 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 12:21:44 -0400 Subject: [PATCH 169/185] The gentle shutdown doesn't really work. There could be too many send messages waiting to be processed. --- .../vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt index a70bbbc7c5..476f17d12f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpWebSocket.kt @@ -109,7 +109,8 @@ class OkHttpWebSocket( } override fun disconnect() { - socket?.close(1000, "Normal closure") + // uses cancel to kill the SEND stack that might be waiting + socket?.cancel() } override fun send(msg: String): Boolean = socket?.send(msg) ?: false From ba460e011ef97ac15e350127da5c31150ef495b8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 12:22:22 -0400 Subject: [PATCH 170/185] Adds a lower boundary for inbox from random relays. --- .../AccountNotificationsEoseFromRandomRelaysManager.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt index 19ab9b330d..740d93c9cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/nip01Notifications/AccountNotificationsEoseFromRandomRelaysManager.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job @@ -50,7 +51,8 @@ class AccountNotificationsEoseFromRandomRelaysManager( since: SincePerRelayMap?, ): List? = (key.account.followsPerRelay.value.keys - key.account.notificationRelays.flow.value).flatMap { - filterJustTheLatestNotificationsToPubkeyFromRandomRelays(it, user(key).pubkeyHex, since?.get(it)?.time) + val since = since?.get(it)?.time ?: TimeUtils.oneDayAgo() + filterJustTheLatestNotificationsToPubkeyFromRandomRelays(it, user(key).pubkeyHex, since) } val userJobMap = mutableMapOf>() From 14e860928b918371be591106ca91ee199ded72e3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 17:43:31 -0400 Subject: [PATCH 171/185] Improves names of the repost methods --- .../vitorpamplona/amethyst/model/LocalCache.kt | 12 ++++++++---- .../quartz/nip18Reposts/GenericRepostEvent.kt | 15 ++++++++------- .../quartz/nip18Reposts/RepostEvent.kt | 15 ++++++++------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 80e089a29f..ac7867b3c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -800,11 +800,15 @@ object LocalCache : ILocalCache { is BadgeAwardEvent -> event.awardDefinition().map { getOrCreateAddressableNote(it) } is PrivateDmEvent -> event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) } is RepostEvent -> - event.boostedEventIds().mapNotNull { checkGetOrCreateNote(it) } + - event.boostedAddresses().map { getOrCreateAddressableNote(it) } + listOfNotNull( + event.boostedEventId()?.let { checkGetOrCreateNote(it) }, + event.boostedAddress()?.let { getOrCreateAddressableNote(it) }, + ) is GenericRepostEvent -> - event.boostedEventIds().mapNotNull { checkGetOrCreateNote(it) } + - event.boostedAddresses().map { getOrCreateAddressableNote(it) } + listOfNotNull( + event.boostedEventId()?.let { checkGetOrCreateNote(it) }, + event.boostedAddress()?.let { getOrCreateAddressableNote(it) }, + ) is CommunityPostApprovalEvent -> event.approvedEvents().mapNotNull { checkGetOrCreateNote(it) } + event.approvedAddresses().map { getOrCreateAddressableNote(it) } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt index 0533215638..acf7e2b1b0 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/GenericRepostEvent.kt @@ -41,6 +41,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull @Immutable class GenericRepostEvent( @@ -66,18 +67,18 @@ class GenericRepostEvent( override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) - fun boostedEvents() = tags.mapNotNull(ETag::parse) + fun boostedEvent() = tags.lastNotNullOfOrNull(ETag::parse) - fun boostedATags() = tags.mapNotNull(ATag::parse) + fun boostedATag() = tags.lastNotNullOfOrNull(ATag::parse) - fun boostedAddresses() = tags.mapNotNull(ATag::parseAddress) + fun boostedAddress() = tags.lastNotNullOfOrNull(ATag::parseAddress) + + fun boostedEventId() = tags.lastNotNullOfOrNull(ETag::parseId) + + fun boostedAddressIds() = tags.lastNotNullOfOrNull(ATag::parseAddressId) fun originalAuthors() = tags.mapNotNull(PTag::parse) - fun boostedEventIds() = tags.mapNotNull(ETag::parseId) - - fun boostedAddressIds() = tags.mapNotNull(ATag::parseAddressId) - fun originalAuthorKeys() = tags.mapNotNull(PTag::parseKey) fun containedPost() = diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt index 922952b395..c7feb946ba 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip18Reposts/RepostEvent.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.pTag import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils +import com.vitorpamplona.quartz.utils.lastNotNullOfOrNull @Immutable class RepostEvent( @@ -64,18 +65,18 @@ class RepostEvent( override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) - fun boostedEvents() = tags.mapNotNull(ETag::parse) + fun boostedEvent() = tags.lastNotNullOfOrNull(ETag::parse) - fun boostedATags() = tags.mapNotNull(ATag::parse) + fun boostedATag() = tags.lastNotNullOfOrNull(ATag::parse) - fun boostedAddresses() = tags.mapNotNull(ATag::parseAddress) + fun boostedAddress() = tags.lastNotNullOfOrNull(ATag::parseAddress) + + fun boostedEventId() = tags.lastNotNullOfOrNull(ETag::parseId) + + fun boostedAddressIds() = tags.lastNotNullOfOrNull(ATag::parseAddressId) fun originalAuthors() = tags.mapNotNull(PTag::parse) - fun boostedEventIds() = tags.mapNotNull(ETag::parseId) - - fun boostedAddressIds() = tags.mapNotNull(ATag::parseAddressId) - fun originalAuthorKeys() = tags.mapNotNull(PTag::parseKey) fun containedPost() = From fe8f519dbffef9014615ef9609b9c7c94b79fd1d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 17:45:21 -0400 Subject: [PATCH 172/185] Correctly logs off from the current account when creating new accounts from inside an existing account. --- .../amethyst/LocalPreferences.kt | 4 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 2 +- .../amethyst/ui/navigation/AppNavigation.kt | 8 +- .../ui/screen/AccountStateViewModel.kt | 91 +++++++++++-------- 4 files changed, 58 insertions(+), 47 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 6fd951d95a..62507b79fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -270,7 +270,7 @@ object LocalPreferences { * deleted */ @SuppressLint("ApplySharedPref") - suspend fun updatePrefsForLogout(accountInfo: AccountInfo) { + suspend fun deleteAccount(accountInfo: AccountInfo) { Log.d("LocalPreferences", "Saving to encrypted storage updatePrefsForLogout ${accountInfo.npub}") withContext(Dispatchers.IO) { encryptedPreferences(accountInfo.npub).edit(commit = true) { clear() } @@ -285,7 +285,7 @@ object LocalPreferences { } } - suspend fun updatePrefsForLogin(accountSettings: AccountSettings) { + suspend fun setDefaultAccount(accountSettings: AccountSettings) { setCurrentAccount(accountSettings) saveToEncryptedStorage(accountSettings) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 269e4ed12e..8e0703715c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -76,7 +76,7 @@ class MainActivity : AppCompatActivity() { val accountStateViewModel: AccountStateViewModel = viewModel() LaunchedEffect(key1 = Unit) { - accountStateViewModel.tryLoginExistingAccountAsync() + accountStateViewModel.loginWithDefaultAccountIfLoggedOff() } AccountScreen(accountStateViewModel, sharedPreferencesViewModel) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 89fefa6780..bcc6cc9f90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -297,8 +297,8 @@ private fun NavigateIfIntentRequested( if (actionableNextPage != null) { actionableNextPage?.let { nextRoute -> val npub = runCatching { URI(intentNextPage.removePrefix("nostr:")).findParameterValue("account") }.getOrNull() - if (npub != null && accountStateViewModel.currentAccount() != npub) { - accountStateViewModel.switchUserSync(npub, nextRoute) + if (npub != null && accountStateViewModel.currentAccountNPub() != npub) { + accountStateViewModel.checkAndSwitchUserSync(npub, nextRoute) } else { val currentRoute = getRouteWithArguments(nav.controller) if (!isSameRoute(currentRoute, nextRoute)) { @@ -353,8 +353,8 @@ private fun NavigateIfIntentRequested( if (newPage != null) { scope.launch { val npub = runCatching { URI(uri.removePrefix("nostr:")).findParameterValue("account") }.getOrNull() - if (npub != null && accountStateViewModel.currentAccount() != npub) { - accountStateViewModel.switchUserSync(npub, newPage) + if (npub != null && accountStateViewModel.currentAccountNPub() != npub) { + accountStateViewModel.checkAndSwitchUserSync(npub, newPage) } else { val currentRoute = getRouteWithArguments(nav.controller) if (!isSameRoute(currentRoute, newPage)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt index d471791146..cbaecf3f0e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.model.DefaultDMRelayList import com.vitorpamplona.amethyst.model.DefaultNIP65List import com.vitorpamplona.amethyst.model.DefaultNIP65RelaySet import com.vitorpamplona.amethyst.model.DefaultSearchRelayList +import com.vitorpamplona.amethyst.model.preferences.AccountPreferenceStores.Companion.torSettings import com.vitorpamplona.amethyst.service.Nip05NostrAddressVerifier import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.tor.TorSettings @@ -51,6 +52,7 @@ import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress import com.vitorpamplona.quartz.nip19Bech32.entities.NEmbed import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay @@ -85,16 +87,16 @@ class AccountStateViewModel : ViewModel() { private var collectorJob: Job? = null - fun tryLoginExistingAccountAsync() { + fun loginWithDefaultAccountIfLoggedOff() { // pulls account from storage. if (_accountContent.value !is AccountState.LoggedIn) { viewModelScope.launch { - tryLoginExistingAccount() + loginWithDefaultAccount() } } } - private suspend fun tryLoginExistingAccount(route: Route? = null) { + private suspend fun loginWithDefaultAccount(route: Route? = null) { val accountSettings = withContext(Dispatchers.IO) { LocalPreferences.loadCurrentAccountFromEncryptedStorage() @@ -107,9 +109,7 @@ class AccountStateViewModel : ViewModel() { } } - private suspend fun requestLoginUI() { - _accountContent.update { AccountState.LoggedOff } - } + private suspend fun requestLoginUI() = _accountContent.update { AccountState.LoggedOff } suspend fun loginAndStartUI( key: String, @@ -124,7 +124,7 @@ class AccountStateViewModel : ViewModel() { is NSec -> null is NPub -> parsed.hex.hexToByteArray() is NProfile -> parsed.hex.hexToByteArray() - is com.vitorpamplona.quartz.nip19Bech32.entities.NNote -> null + is NNote -> null is NEvent -> null is NEmbed -> null is NRelay -> null @@ -175,7 +175,7 @@ class AccountStateViewModel : ViewModel() { ) } - LocalPreferences.updatePrefsForLogin(account) + LocalPreferences.setDefaultAccount(account) startUI(account) } @@ -277,6 +277,9 @@ class AccountStateViewModel : ViewModel() { onError: (String) -> Unit, ) { try { + if (_accountContent.value is AccountState.LoggedIn) { + prepareLogoutOrSwitch() + } loginAndStartUI(key, torSettings, transientAccount, loginWithExternalSigner, packageName) } catch (e: Exception) { if (e is CancellationException) throw e @@ -290,31 +293,15 @@ class AccountStateViewModel : ViewModel() { name: String? = null, ) { viewModelScope.launch(Dispatchers.IO) { + if (_accountContent.value is AccountState.LoggedIn) { + prepareLogoutOrSwitch() + } + _accountContent.update { AccountState.Loading } - val keyPair = KeyPair() - val tempSigner = NostrSignerSync(keyPair) + val accountSettings = createNewAccount(torSettings, name) - val accountSettings = - AccountSettings( - keyPair = keyPair, - transientAccount = false, - backupUserMetadata = tempSigner.sign(MetadataEvent.newUser(name)), - backupContactList = - ContactListEvent.createFromScratch( - followUsers = listOf(ContactTag(keyPair.pubKey.toHexKey(), null, null)), - relayUse = emptyMap(), - signer = tempSigner, - ), - backupNIP65RelayList = AdvertisedRelayListEvent.create(DefaultNIP65List, tempSigner), - backupDMRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, tempSigner), - backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), tempSigner), - backupChannelList = ChannelListEvent.create(emptyList(), DefaultChannels, tempSigner), - torSettings = TorSettingsFlow.build(torSettings), - ) - - // saves to local preferences - LocalPreferences.updatePrefsForLogin(accountSettings) + LocalPreferences.setDefaultAccount(accountSettings) startUI(accountSettings) @@ -333,13 +320,38 @@ class AccountStateViewModel : ViewModel() { } } + fun createNewAccount( + torSettings: TorSettings, + name: String? = null, + ): AccountSettings { + val keyPair = KeyPair() + val tempSigner = NostrSignerSync(keyPair) + + return AccountSettings( + keyPair = keyPair, + transientAccount = false, + backupUserMetadata = tempSigner.sign(MetadataEvent.newUser(name)), + backupContactList = + ContactListEvent.createFromScratch( + followUsers = listOf(ContactTag(keyPair.pubKey.toHexKey(), null, null)), + relayUse = emptyMap(), + signer = tempSigner, + ), + backupNIP65RelayList = AdvertisedRelayListEvent.create(DefaultNIP65List, tempSigner), + backupDMRelayList = ChatMessageRelayListEvent.create(DefaultDMRelayList, tempSigner), + backupSearchRelayList = SearchRelayListEvent.create(DefaultSearchRelayList.toList(), tempSigner), + backupChannelList = ChannelListEvent.create(emptyList(), DefaultChannels, tempSigner), + torSettings = TorSettingsFlow.build(torSettings), + ) + } + fun switchUser(accountInfo: AccountInfo) { viewModelScope.launch(Dispatchers.IO) { switchUserSync(accountInfo) } } - suspend fun switchUserSync( + suspend fun checkAndSwitchUserSync( npub: String, route: Route, ): Boolean { @@ -353,34 +365,33 @@ class AccountStateViewModel : ViewModel() { return false } - suspend fun switchUserSync( + private suspend fun switchUserSync( accountInfo: AccountInfo, route: Route? = null, ) { prepareLogoutOrSwitch() LocalPreferences.switchToAccount(accountInfo) - tryLoginExistingAccount(route) + loginWithDefaultAccount(route) } - fun currentAccount() = + fun currentAccountNPub() = when (val state = _accountContent.value) { is AccountState.LoggedIn -> state.accountSettings.keyPair.pubKey .toNpub() - else -> null } fun logOff(accountInfo: AccountInfo) { viewModelScope.launch(Dispatchers.IO) { - if (accountInfo.npub == currentAccount()) { + if (accountInfo.npub == currentAccountNPub()) { // log off and relogin with the 0 account prepareLogoutOrSwitch() - LocalPreferences.updatePrefsForLogout(accountInfo) - tryLoginExistingAccount() + LocalPreferences.deleteAccount(accountInfo) + loginWithDefaultAccount() } else { - // delete without login off - LocalPreferences.updatePrefsForLogout(accountInfo) + // delete without switching logins + LocalPreferences.deleteAccount(accountInfo) } } } From c5645b0ae7dab7971197402fcf0f5af1f08cb9bd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 17:45:44 -0400 Subject: [PATCH 173/185] Removes suspend of NostrSignerSync-based functions --- .../nip02FollowList/ContactListEvent.kt | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt index 77bbc010e9..edf55518c8 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip02FollowList/ContactListEvent.kt @@ -94,21 +94,6 @@ class ContactListEvent( fun blockListFor(pubKeyHex: HexKey): String = "3:$pubKeyHex:" - suspend fun createFromScratch( - followUsers: List = emptyList(), - relayUse: Map? = emptyMap(), - signer: NostrSignerSync, - createdAt: Long = TimeUtils.now(), - ): ContactListEvent { - val content = relayUse?.let { RelaySet.assemble(it) } ?: "" - - val tags = - listOf(AltTag.assemble(ALT)) + - followUsers.map { it.toTagArray() } - - return signer.sign(createdAt, KIND, tags.toTypedArray(), content) - } - suspend fun createFromScratch( followUsers: List, relayUse: Map?, @@ -189,5 +174,20 @@ class ContactListEvent( return signer.sign(createdAt, KIND, newTags, content) } + + fun createFromScratch( + followUsers: List = emptyList(), + relayUse: Map? = emptyMap(), + signer: NostrSignerSync, + createdAt: Long = TimeUtils.now(), + ): ContactListEvent { + val content = relayUse?.let { RelaySet.assemble(it) } ?: "" + + val tags = + listOf(AltTag.assemble(ALT)) + + followUsers.map { it.toTagArray() } + + return signer.sign(createdAt, KIND, tags.toTypedArray(), content) + } } } From 141f0ed63bd8760a12f6f026eb57acc2c93f81e4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 19:06:09 -0400 Subject: [PATCH 174/185] Removes unused items from Node --- .../src/main/java/com/vitorpamplona/amethyst/model/Note.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 204d98c5b8..90de133f3b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -67,7 +67,6 @@ import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.nip71Video.VideoEvent import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent -import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.anyAsync import com.vitorpamplona.quartz.utils.containsAny @@ -156,8 +155,6 @@ open class Note( var relays = listOf() private set - fun id() = Hex.decode(idHex) - open fun idNote() = toNEvent() open fun toNEvent(): String { @@ -955,7 +952,6 @@ class NoteFlowSet( metadata.hasObservers() || reports.hasObservers() || relays.hasObservers() || - metadata.hasObservers() || reactions.hasObservers() || boosts.hasObservers() || replies.hasObservers() || From 7e9d5d60639a01bd6cc7a2248507b5c97fa7ff8e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 19:06:34 -0400 Subject: [PATCH 175/185] Uses new tag model on the ZapEvent --- .../java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt index d233b4ee18..5393d2b4da 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt @@ -89,9 +89,9 @@ class LnZapEvent( zapRequest = containedPost() } - override fun zappedPost() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + override fun zappedPost() = tags.mapNotNull(ETag::parseId) - override fun zappedAuthor() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } + override fun zappedAuthor() = tags.mapNotNull(PTag::parseKey) override fun zappedPollOption(): Int? = try { From e1b5f0f7b531390176d77d102600d43b8898145e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 19:11:08 -0400 Subject: [PATCH 176/185] Renames chatroom and message fields --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../vitorpamplona/amethyst/model/LocalCache.kt | 8 ++------ .../amethyst/model/privateChats/Chatroom.kt | 18 +++++++++--------- .../model/privateChats/ChatroomList.kt | 14 +++++++------- .../notifications/EventNotificationConsumer.kt | 6 +++--- .../chats/privateDM/dal/ChatroomFeedFilter.kt | 8 ++++---- .../header/NewChatroomSubjectDialog.kt | 2 +- .../rooms/dal/ChatroomListKnownFeedFilter.kt | 4 ++-- .../rooms/dal/ChatroomListNewFeedFilter.kt | 4 ++-- 9 files changed, 31 insertions(+), 35 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index b805dc2c91..f0c81c0462 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -317,7 +317,7 @@ class Account( val privateZapsDecryptionCache = PrivateZapCache(signer) val draftsDecryptionCache = DraftEventCache(signer) - val chatroomList = LocalCache.getOrCreateChatroomList(signer.pubKey) + val chatroomList = cache.getOrCreateChatroomList(signer.pubKey) val privacyState = PrivacyState(settings) val torRelayState = TorRelayState(trustedRelays, dmRelayList, settings, scope) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index ac7867b3c5..e3e40c6aac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -389,8 +389,6 @@ object LocalCache : ILocalCache { idHex: String, note: Note, ): Note { - checkNotInMainThread() - require(isValidHex(idHex)) { "$idHex is not a valid hex" } return notes.getOrCreate(idHex) { @@ -399,8 +397,6 @@ object LocalCache : ILocalCache { } fun getOrCreateNote(idHex: String): Note { - checkNotInMainThread() - require(isValidHex(idHex)) { "$idHex is not a valid hex" } return notes.getOrCreate(idHex) { @@ -2313,7 +2309,7 @@ object LocalCache : ILocalCache { } chatroomList.forEach { userHex, room -> - room.chatrooms.map { key, chatroom -> + room.rooms.map { key, chatroom -> val toBeRemoved = chatroom.pruneMessagesToTheLatestOnly() val childrenToBeRemoved = mutableListOf() @@ -2330,7 +2326,7 @@ object LocalCache : ILocalCache { if (toBeRemoved.size > 1) { println( - "PRUNE: ${toBeRemoved.size} private messages from $userHex to ${key.users.joinToString()} removed. ${chatroom.roomMessages.size} kept", + "PRUNE: ${toBeRemoved.size} private messages from $userHex to ${key.users.joinToString()} removed. ${chatroom.messages.size} kept", ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index d956b21dce..f7bfb7df22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @Stable class Chatroom { var activeSenders: Set = setOf() - var roomMessages: Set = setOf() + var messages: Set = setOf() var subject = MutableStateFlow(null) var subjectCreatedAt: Long? = null var ownerSentMessage: Boolean = false @@ -42,8 +42,8 @@ class Chatroom { @Synchronized fun addMessageSync(msg: Note) { - if (msg !in roomMessages) { - roomMessages = roomMessages + msg + if (msg !in messages) { + messages = messages + msg msg.author?.let { author -> if (author !in activeSenders) { @@ -69,10 +69,10 @@ class Chatroom { fun removeMessageSync(msg: Note) { checkNotInMainThread() - if (msg in roomMessages) { - roomMessages = roomMessages - msg + if (msg in messages) { + messages = messages - msg - roomMessages + messages .filter { it.event?.subject() != null } .sortedBy { it.createdAt() } .lastOrNull() @@ -86,7 +86,7 @@ class Chatroom { fun senderIntersects(keySet: Set): Boolean = activeSenders.any { it.pubkeyHex in keySet } fun pruneMessagesToTheLatestOnly(): Set { - val sorted = roomMessages.sortedWith(DefaultFeedOrder) + val sorted = messages.sortedWith(DefaultFeedOrder) val toKeep = if ((sorted.firstOrNull()?.createdAt() ?: 0) > TimeUtils.oneWeekAgo()) { @@ -97,8 +97,8 @@ class Chatroom { sorted.take(1).toSet() } + sorted.filter { it.flowSet?.isInUse() ?: false } + sorted.filter { it.event !is PrivateDmEvent } - val toRemove = roomMessages.minus(toKeep) - roomMessages = toKeep + val toRemove = messages.minus(toKeep) + messages = toKeep return toRemove } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt index f73f4790e6..93bcdc372b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt @@ -30,10 +30,10 @@ import kotlinx.collections.immutable.persistentSetOf class ChatroomList( val ownerPubKey: HexKey, ) { - var chatrooms = LargeCache() + var rooms = LargeCache() private set - private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom = chatrooms.getOrCreate(key) { Chatroom() } + private fun getOrCreatePrivateChatroomSync(key: ChatroomKey): Chatroom = rooms.getOrCreate(key) { Chatroom() } fun getOrCreatePrivateChatroom(user: User): Chatroom { val key = ChatroomKey(persistentSetOf(user.pubkeyHex)) @@ -47,7 +47,7 @@ class ChatroomList( msg: Note, ) { val privateChatroom = getOrCreatePrivateChatroom(room) - if (msg !in privateChatroom.roomMessages) { + if (msg !in privateChatroom.messages) { privateChatroom.addMessageSync(msg) } } @@ -57,7 +57,7 @@ class ChatroomList( msg: Note, ) { val privateChatroom = getOrCreatePrivateChatroom(user) - if (msg !in privateChatroom.roomMessages) { + if (msg !in privateChatroom.messages) { privateChatroom.addMessageSync(msg) if (msg.author?.pubkeyHex == ownerPubKey) { privateChatroom.ownerSentMessage = true @@ -70,7 +70,7 @@ class ChatroomList( msg: Note, ) { val privateChatroom = getOrCreatePrivateChatroom(user) - if (msg in privateChatroom.roomMessages) { + if (msg in privateChatroom.messages) { privateChatroom.removeMessageSync(msg) } } @@ -80,13 +80,13 @@ class ChatroomList( msg: Note, ) { val privateChatroom = getOrCreatePrivateChatroom(room) - if (msg in privateChatroom.roomMessages) { + if (msg in privateChatroom.messages) { privateChatroom.removeMessageSync(msg) } } fun hasSentMessagesTo(key: ChatroomKey?): Boolean { if (key == null) return false - return chatrooms.get(key)?.ownerSentMessage == true + return rooms.get(key)?.ownerSentMessage == true } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 75eeeff215..625c6cf2d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -203,7 +203,7 @@ class EventNotificationConsumer( val isKnownRoom = ( - chatroomList.chatrooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) + chatroomList.rooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) ) if (isKnownRoom) { @@ -246,7 +246,7 @@ class EventNotificationConsumer( val followingKeySet = acc.backupContactList?.unverifiedFollowKeySet()?.toSet() ?: return - val isKnownRoom = chatroomList.chatrooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) + val isKnownRoom = chatroomList.rooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) if (isKnownRoom) { val content = chatNote.event?.content ?: "" @@ -284,7 +284,7 @@ class EventNotificationConsumer( val chatRoom = event.chatroomKey(signer.pubKey) - val isKnownRoom = chatroomList.chatrooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) + val isKnownRoom = chatroomList.rooms.get(chatRoom)?.senderIntersects(followingKeySet) == true || chatroomList.hasSentMessagesTo(chatRoom) if (isKnownRoom) { note.author?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt index bf5808d443..3f24867d4b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedFilter.kt @@ -34,18 +34,18 @@ class ChatroomFeedFilter( override fun feedKey(): String = withUser.hashCode().toString() override fun feed(): List { - val messages = account.chatroomList.getOrCreatePrivateChatroom(withUser) + val chatroom = account.chatroomList.getOrCreatePrivateChatroom(withUser) - return messages.roomMessages + return chatroom.messages .filter { account.isAcceptable(it) } .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) .reversed() } override fun applyFilter(collection: Set): Set { - val messages = account.chatroomList.getOrCreatePrivateChatroom(withUser) + val chatroom = account.chatroomList.getOrCreatePrivateChatroom(withUser) - return collection.filter { it in messages.roomMessages && account.isAcceptable(it) }.toSet() + return collection.filter { it in chatroom.messages && account.isAcceptable(it) }.toSet() } override fun sort(collection: Set): List = collection.sortedWith(DefaultFeedOrder) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt index 6f59c7f164..15f12dc00a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/header/NewChatroomSubjectDialog.kt @@ -74,7 +74,7 @@ fun NewChatroomSubjectDialog( val groupName = remember { mutableStateOf( - accountViewModel.account.chatroomList.chatrooms + accountViewModel.account.chatroomList.rooms .get(room) ?.subject ?.value ?: "", diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt index b59fbe5d8e..a3773be358 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListKnownFeedFilter.kt @@ -43,7 +43,7 @@ class ChatroomListKnownFeedFilter( val followingKeySet = account.followingKeySet() val privateMessages = - chatList.chatrooms.mapNotNull { key, chatroom -> + chatList.rooms.mapNotNull { key, chatroom -> if ((chatroom.senderIntersects(followingKeySet) || chatList.hasSentMessagesTo(key)) && !account.isAllHidden(key.users) ) { @@ -229,7 +229,7 @@ class ChatroomListKnownFeedFilter( .forEach { newNote -> val roomKey = (newNote.event as? ChatroomKeyable)?.chatroomKey(me.pubkeyHex) if (roomKey != null) { - val room = account.chatroomList.chatrooms.get(roomKey) + val room = account.chatroomList.rooms.get(roomKey) if (room != null) { if ( ( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt index a7b1e5a336..969695f432 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/dal/ChatroomListNewFeedFilter.kt @@ -39,7 +39,7 @@ class ChatroomListNewFeedFilter( val followingKeySet = account.followingKeySet() val privateMessages = - chatList.chatrooms.mapNotNull { key, chatroom -> + chatList.rooms.mapNotNull { key, chatroom -> if (!chatroom.senderIntersects(followingKeySet) && !chatList.hasSentMessagesTo(key) && !account.isAllHidden(key.users)) { chatroom.lastMessage } else { @@ -108,7 +108,7 @@ class ChatroomListNewFeedFilter( val noteEvent = newNote.event if (noteEvent is ChatroomKeyable) { val roomKey = noteEvent.chatroomKey(me.pubkeyHex) - val room = account.chatroomList.chatrooms.get(roomKey) + val room = account.chatroomList.rooms.get(roomKey) if (room != null && ( From e82c09e1c3d1bf97f47f5e8f4bb875ecf3e04125 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 19:11:55 -0400 Subject: [PATCH 177/185] Finishes the migration of adressable ids on the local cache. --- .../amethyst/ui/actions/NewMessageTagger.kt | 10 ++++++---- .../amethyst/ui/components/ClickableRoute.kt | 2 +- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 11 +---------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt index e9812406bc..9ac7dfe6bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMessageTagger.kt @@ -21,10 +21,12 @@ package com.vitorpamplona.amethyst.ui.actions import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.crypto.Nip01 +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser import com.vitorpamplona.quartz.nip19Bech32.bech32.Bech32 import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes @@ -77,7 +79,7 @@ class NewMessageTagger( is NEmbed -> addNoteToReplyTos(dao.getOrCreateNote(entity.event.id)) is NAddress -> { - val note = dao.checkGetOrCreateAddressableNote(entity.aTag()) + val note = dao.getOrCreateAddressableNote(entity.address()) if (note != null) { addNoteToReplyTos(note) } @@ -114,9 +116,9 @@ class NewMessageTagger( } is NAddress -> { - val note = dao.checkGetOrCreateAddressableNote(entity.aTag()) + val note = dao.getOrCreateAddressableNote(entity.address()) if (note != null) { - getNostrAddress(note.idNote(), results.restOfWord) + getNostrAddress(note.toNAddr(), results.restOfWord) } else { word } @@ -234,5 +236,5 @@ interface Dao { suspend fun getOrCreateNote(hex: String): Note - suspend fun checkGetOrCreateAddressableNote(hex: String): Note? + suspend fun getOrCreateAddressableNote(address: Address): AddressableNote? } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index a3388f6ce0..1e304ab32e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -205,7 +205,7 @@ private fun DisplayAddress( if (noteBase == null) { LaunchedEffect(key1 = nip19) { - accountViewModel.checkGetOrCreateAddressableNote(nip19.aTag()) { noteBase = it } + noteBase = accountViewModel.getOrCreateAddressableNote(nip19.address()) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 690928c228..3f5079dfc8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -1056,16 +1056,7 @@ class AccountViewModel( fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex) - override suspend fun checkGetOrCreateAddressableNote(key: HexKey): AddressableNote? = LocalCache.checkGetOrCreateAddressableNote(key) - - fun checkGetOrCreateAddressableNote( - key: HexKey, - onResult: (AddressableNote?) -> Unit, - ) { - viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateAddressableNote(key)) } - } - - suspend fun getOrCreateAddressableNote(key: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(key) + override suspend fun getOrCreateAddressableNote(address: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(address) fun getOrCreateAddressableNote( key: Address, From add8f8eba23ba746982e4f5ddcab59d753d79fb1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 19:12:10 -0400 Subject: [PATCH 178/185] Improves the rendering of the node id --- .../amethyst/ui/components/RichTextViewer.kt | 30 +++++++++++++++++++ .../amethyst/ui/note/PubKeyFormatter.kt | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 46b0e71b94..da20d34cd3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -81,6 +81,7 @@ import com.vitorpamplona.amethyst.commons.richtext.SecretEmoji import com.vitorpamplona.amethyst.commons.richtext.Segment import com.vitorpamplona.amethyst.commons.richtext.WithdrawSegment import com.vitorpamplona.amethyst.model.HashtagIcon +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon @@ -89,6 +90,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor @@ -100,10 +102,12 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.theme.CashuCardBorders import com.vitorpamplona.amethyst.ui.theme.HalfVertPadding +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.inlinePlaceholder import com.vitorpamplona.amethyst.ui.theme.innerPostModifier import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -744,6 +748,32 @@ fun TagLink( } } +@Preview +@Composable +fun DisplayNoteFromTagPreview() { + val dummyPost = + TextNoteEvent( + id = "0b6d941c46411a95edb1c93da7ad6ca26370497d8c7b7d621f5cb59f48841bad", + pubKey = "6dd3b72e325da7383b275eef1c66131ba4664326e162bc060527509b4e33ae43", + createdAt = 1753988264, + tags = emptyArray(), + content = "test", + sig = "ec39e60722a083cccbd2d82d2827e13f5499fa7cbcedac5b76011a844c077473adb629d50d01fab147835ac6c8a3d5ba9aaddd87d6723f0c3c864b9119fc4356", + ) + + LocalCache.justConsume(dummyPost, null, true) + val note = LocalCache.getOrCreateNote(dummyPost.id) + + ThemeComparisonColumn( + toPreview = { + ClickableTextPrimary( + text = "@${note.idNote().toShortDisplay()}", + onClick = { }, + ) + }, + ) +} + @Composable private fun DisplayNoteFromTag( baseNote: Note, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt index 4ba6a2f3f8..9e18f7e2f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/PubKeyFormatter.kt @@ -27,7 +27,7 @@ fun ByteArray.toHexShortDisplay(): String = toHexKey().toShortDisplay() fun String.toShortDisplay(): String { if (length <= 16) return this - return replaceRange(8, length - 8, ":") + return replaceRange(8, length - 8, "…") } fun HexKey.toDisplayHexKey(): String = this.toShortDisplay() From 42738e72f7d7dd0ccc8ff3df84ceb421cd9b0a98 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 31 Jul 2025 19:14:43 -0400 Subject: [PATCH 179/185] fixes test case --- .../vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt index aad61b5cab..8ec1602d0c 100644 --- a/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt +++ b/amethyst/src/test/java/com/vitorpamplona/amethyst/NewMessageTaggerKeyParseTest.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger +import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import org.junit.Assert.assertEquals @@ -43,9 +44,9 @@ class NewMessageTaggerKeyParseTest { com.vitorpamplona.amethyst.model .Note(hex) - override suspend fun checkGetOrCreateAddressableNote(hex: String) = + override suspend fun getOrCreateAddressableNote(address: Address) = com.vitorpamplona.amethyst.model - .Note(hex) + .AddressableNote(address) } @Test From e715a74bc429ef787f77d5d9e6bb4744719ff0af Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Aug 2025 08:30:44 -0400 Subject: [PATCH 180/185] Better indexes notes in channels and chatrooms --- .../com/vitorpamplona/amethyst/model/Note.kt | 3 + .../amethyst/model/privateChats/Chatroom.kt | 11 +- .../loggedIn/DecryptAndIndexProcessor.kt | 100 ++++++------------ 3 files changed, 43 insertions(+), 71 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 90de133f3b..7e420a4e82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState +import com.vitorpamplona.amethyst.model.privateChats.Chatroom import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.service.replace @@ -130,6 +131,8 @@ open class Note( var event: Event? = null var author: User? = null var replyTo: List? = null + var inChannel: Channel? = null + var inChatroom: Chatroom? = null // These fields are updated every time an event related to this note is received. var replies = listOf() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index f7bfb7df22..d562e8ce7f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.model.privateChats import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent @@ -41,7 +40,7 @@ class Chatroom { var lastMessage: Note? = null @Synchronized - fun addMessageSync(msg: Note) { + fun addMessageSync(msg: Note): Boolean { if (msg !in messages) { messages = messages + msg @@ -62,13 +61,13 @@ class Chatroom { subject.tryEmit(newSubject) subjectCreatedAt = msg.createdAt() } + return true } + return false } @Synchronized - fun removeMessageSync(msg: Note) { - checkNotInMainThread() - + fun removeMessageSync(msg: Note): Boolean { if (msg in messages) { messages = messages - msg @@ -80,7 +79,9 @@ class Chatroom { subject.tryEmit(it.event?.subject()) subjectCreatedAt = it.createdAt() } + return true } + return false } fun senderIntersects(keySet: Set): Boolean = activeSenders.any { it.pubkeyHex in keySet } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 13f513dd29..b12107f36c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent @@ -92,10 +91,9 @@ class DecryptAndIndexProcessor( // Avoid decrypting over and over again if the event already exist. if (event.pubKey == account.signer.pubKey) { if (!event.isDeleted()) { - if (account.draftsDecryptionCache.preCachedDraft(event) == null) { - account.draftsDecryptionCache.cachedDraft(event)?.let { rumor -> - indexDraftAsRealEvent(eventNote, rumor) - } + val rumor = account.draftsDecryptionCache.preCachedDraft(event) ?: account.draftsDecryptionCache.cachedDraft(event) + if (rumor != null) { + indexDraftAsRealEvent(eventNote, rumor) } } } @@ -200,14 +198,7 @@ class DecryptAndIndexProcessor( is DraftEvent -> { // Avoid decrypting over and over again if the event already exist. if (event.pubKey == account.signer.pubKey) { - if (!event.isDeleted()) { - if (account.draftsDecryptionCache.preCachedDraft(event) == null) { - account.draftsDecryptionCache.cachedDraft(event)?.let { - deindexDraftAsRealEvent(eventNote, it) - } - account.draftsDecryptionCache.delete(event) - } - } + deindexDraftAsRealEvent(eventNote) } } @@ -257,90 +248,67 @@ class DecryptAndIndexProcessor( draftEventWrap: Note, rumor: Event, ) { + draftEventWrap.replyTo = cache.computeReplyTo(rumor) + draftEventWrap.replyTo?.forEach { it.addReply(draftEventWrap) } + when (rumor) { is PrivateDmEvent -> { if (rumor.canDecrypt(account.signer)) { val talkingWith = rumor.chatroomKey(account.signer.pubKey) - account.chatroomList.addMessage(talkingWith, draftEventWrap) + val chatroom = account.chatroomList.getOrCreatePrivateChatroom(talkingWith) + if (chatroom.addMessageSync(draftEventWrap)) { + draftEventWrap.inChatroom = chatroom + } } } is ChatMessageEvent -> { if (rumor.isIncluded(account.signer.pubKey)) { val key = rumor.chatroomKey(account.signer.pubKey) - account.chatroomList.addMessage(key, draftEventWrap) + val chatroom = account.chatroomList.getOrCreatePrivateChatroom(key) + if (chatroom.addMessageSync(draftEventWrap)) { + draftEventWrap.inChatroom = chatroom + } } } is ChatMessageEncryptedFileHeaderEvent -> { if (rumor.isIncluded(account.signer.pubKey)) { val key = rumor.chatroomKey(account.signer.pubKey) - account.chatroomList.addMessage(key, draftEventWrap) + val chatroom = account.chatroomList.getOrCreatePrivateChatroom(key) + if (chatroom.addMessageSync(draftEventWrap)) { + draftEventWrap.inChatroom = chatroom + } } } is EphemeralChatEvent -> { rumor.roomId()?.let { - cache.getOrCreateEphemeralChannel(it).addNote(draftEventWrap, null) + val channel = cache.getOrCreateEphemeralChannel(it) + channel.addNote(draftEventWrap, null) + draftEventWrap.inChannel = channel } } is ChannelMessageEvent -> { rumor.channelId()?.let { channelId -> - cache.checkGetOrCreatePublicChatChannel(channelId)?.addNote(draftEventWrap, null) + val channel = cache.checkGetOrCreatePublicChatChannel(channelId) + channel?.addNote(draftEventWrap, null) + draftEventWrap.inChannel = channel } } is LiveActivitiesChatMessageEvent -> { rumor.activityAddress()?.let { channelId -> - cache.getOrCreateLiveChannel(channelId).addNote(draftEventWrap, null) + val channel = cache.getOrCreateLiveChannel(channelId) + channel.addNote(draftEventWrap, null) + draftEventWrap.inChannel = channel } } - is TextNoteEvent -> { - val replyTo = cache.computeReplyTo(rumor) - replyTo.forEach { it.addReply(draftEventWrap) } - } } } - fun deindexDraftAsRealEvent( - draftEventWrap: Note, - rumor: Event, - ) { - when (rumor) { - is PrivateDmEvent -> { - if (rumor.canDecrypt(account.signer.pubKey)) { - val talkingWith = rumor.chatroomKey(account.signer.pubKey) - account.chatroomList.addMessage(talkingWith, draftEventWrap) - } - } - is ChatMessageEvent -> { - if (rumor.isIncluded(account.signer.pubKey)) { - val key = rumor.chatroomKey(account.signer.pubKey) - account.chatroomList.removeMessage(key, draftEventWrap) - } - } - is ChatMessageEncryptedFileHeaderEvent -> { - if (rumor.isIncluded(account.signer.pubKey)) { - val key = rumor.chatroomKey(account.signer.pubKey) - account.chatroomList.removeMessage(key, draftEventWrap) - } - } - is ChannelMessageEvent -> { - rumor.channelId()?.let { channelId -> - cache.getPublicChatChannelIfExists(channelId)?.removeNote(draftEventWrap) - } - } - is EphemeralChatEvent -> { - rumor.roomId()?.let { - cache.getEphemeralChatChannelIfExists(it)?.removeNote(draftEventWrap) - } - } - is LiveActivitiesChatMessageEvent -> { - rumor.activityAddress()?.let { channelId -> - cache.getLiveActivityChannelIfExists(channelId)?.removeNote(draftEventWrap) - } - } - is TextNoteEvent -> { - val replyTo = cache.computeReplyTo(rumor) - replyTo.forEach { it.removeReply(draftEventWrap) } - } - } + fun deindexDraftAsRealEvent(draftEventWrap: Note) { + draftEventWrap.replyTo?.forEach { it.removeReply(draftEventWrap) } + draftEventWrap.replyTo = null + + draftEventWrap.inChatroom?.removeMessageSync(draftEventWrap) + draftEventWrap.inChannel?.removeNote(draftEventWrap) } suspend fun runNew(newNotes: Set) { From 4cd24e5363fc3566b6213387858900b04bf0e71e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Aug 2025 09:57:33 -0400 Subject: [PATCH 181/185] No need for not null checks --- amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt | 2 -- .../java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 414abd6bbe..fe3c2f44d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -92,8 +92,6 @@ class User( fun toNostrUri() = "nostr:${toNProfile()}" - override fun toString(): String = pubkeyHex - fun toBestShortFirstName(): String { val fullName = toBestDisplayName() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index 1e45e75392..e403f47965 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -88,7 +88,7 @@ fun WatchAuthorWithBlank( inner(noteAuthor) } else { val authorState by observeNote(baseNote, accountViewModel) - CrossfadeIfEnabled(targetState = authorState?.note?.author, modifier = modifier, label = "WatchAuthorWithBlank", accountViewModel = accountViewModel) { newAuthor -> + CrossfadeIfEnabled(targetState = authorState.note.author, modifier = modifier, label = "WatchAuthorWithBlank", accountViewModel = accountViewModel) { newAuthor -> inner(newAuthor) } } From 26cadfee88061908faa9a14115682ec5e1cd6344 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Aug 2025 11:40:19 -0400 Subject: [PATCH 182/185] Fixes the index after deletions --- .../amethyst/model/LocalCache.kt | 55 ++++++++----------- .../amethyst/ui/feeds/FeedContentState.kt | 7 +++ .../userSuggestions/ShowUserSuggestionList.kt | 14 ++++- .../amethyst/ui/screen/FeedViewModel.kt | 7 +++ .../amethyst/ui/screen/UserFeedViewModel.kt | 7 +++ .../loggedIn/DecryptAndIndexProcessor.kt | 11 +--- .../profile/zaps/LnZapFeedViewModel.kt | 9 +-- .../ui/screen/loggedIn/search/SearchScreen.kt | 18 +++++- .../loggedIn/settings/StringFeedViewModel.kt | 6 ++ 9 files changed, 84 insertions(+), 50 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index e3e40c6aac..55cdc78300 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -200,9 +200,6 @@ import kotlinx.coroutines.launch import java.io.File import java.io.FileOutputStream import java.io.IOException -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter import java.util.concurrent.ConcurrentHashMap interface ILocalCache { @@ -520,12 +517,6 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeBaseReplaceable(event, relay, wasVerified) - fun formattedDateTime(timestamp: Long): String = - Instant - .ofEpochSecond(timestamp) - .atZone(ZoneId.systemDefault()) - .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a")) - fun consume( event: TextNoteEvent, relay: NormalizedRelayUrl? = null, @@ -831,7 +822,7 @@ object LocalCache : ILocalCache { event.mapTaggedEventId { checkGetOrCreateNote(it) } + event.mapTaggedAddress { checkGetOrCreateAddressableNote(it) } } - else -> emptyList() + else -> emptyList() } fun consume( @@ -1339,6 +1330,9 @@ object LocalCache : ILocalCache { masterNote.removeReport(deleteNote) } + deleteNote.inChatroom?.removeMessageSync(deleteNote) + deleteNote.inChannel?.removeNote(deleteNote) + getAnyChannel(deleteNote)?.removeNote(deleteNote) (deletedEvent as? TorrentCommentEvent)?.torrentIds()?.let { @@ -1357,18 +1351,18 @@ object LocalCache : ILocalCache { } fun deleteWraps(event: WrappedEvent) { - event.host?.let { + event.host?.let { hostStub -> // seal - getNoteIfExists(it.id)?.let { - val noteEvent = it.event + getNoteIfExists(hostStub.id)?.let { hostNote -> + val noteEvent = hostNote.event if (noteEvent is WrappedEvent) { deleteWraps(noteEvent) } - it.clearFlow() - refreshDeletedNoteObservers(it) + hostNote.clearFlow() + refreshDeletedNoteObservers(hostNote) } - notes.remove(it.id) + notes.remove(hostStub.id) } } @@ -1590,7 +1584,7 @@ object LocalCache : ILocalCache { oldChannel.updateChannelInfo(author, event) true } else { - wasVerified + false } } else { wasVerified @@ -2055,11 +2049,7 @@ object LocalCache : ILocalCache { if (note.event?.tags?.tagValueContains(text, true) == true || note.idHex.startsWith(text, true) ) { - if (!note.isHiddenFor(hiddenUsers.flow.value)) { - return@filter true - } else { - return@filter false - } + return@filter !note.isHiddenFor(hiddenUsers.flow.value) } if (note.event?.isContentEncoded() == false) { @@ -2080,11 +2070,7 @@ object LocalCache : ILocalCache { if (addressable.event?.tags?.tagValueContains(text, true) == true || addressable.idHex.startsWith(text, true) ) { - if (!addressable.isHiddenFor(hiddenUsers.flow.value)) { - return@filter true - } else { - return@filter false - } + return@filter !addressable.isHiddenFor(hiddenUsers.flow.value) } if (addressable.event?.isContentEncoded() == false) { @@ -2187,7 +2173,7 @@ object LocalCache : ILocalCache { suspend fun findLatestModificationForNote(note: Note): List { checkNotInMainThread() - val originalAuthor = note.author?.pubkeyHex ?: return emptyList() + val noteAuthor = note.author ?: return emptyList() modificationCache[note.idHex]?.let { return it @@ -2200,7 +2186,7 @@ object LocalCache : ILocalCache { .filter { _, item -> val noteEvent = item.event - noteEvent is TextNoteModificationEvent && note.author == item.author && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) + noteEvent is TextNoteModificationEvent && noteAuthor == item.author && noteEvent.isTaggedEvent(note.idHex) && !noteEvent.isExpirationBefore(time) }.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) modificationCache.put(note.idHex, newNotes) @@ -2315,8 +2301,7 @@ object LocalCache : ILocalCache { val childrenToBeRemoved = mutableListOf() toBeRemoved.forEach { - // TODO: NEED TO TEST IF WRAPS COME BACK WHEN NEEDED BEFORE ACTIVATING - // childrenToBeRemoved.addAll(removeIfWrap(it)) + childrenToBeRemoved.addAll(removeIfWrap(it)) removeFromCache(it) childrenToBeRemoved.addAll(it.removeAllChildNotes()) @@ -2428,6 +2413,9 @@ object LocalCache : ILocalCache { masterNote.removeReport(note) } + note.inChatroom?.removeMessageSync(note) + note.inChannel?.removeNote(note) + val noteEvent = note.event if (noteEvent is LnZapEvent) { @@ -2896,6 +2884,9 @@ class LocalCacheFlow { // Refreshes observers in batches. private val bundler = BundledInsert(1000, Dispatchers.Default) + // Refreshes observers in batches. + private val bundler2 = BundledInsert(1000, Dispatchers.Default) + fun newNote(newNote: Note) { bundler.invalidateList(newNote) { bundledNewNotes -> _newEventBundles.emit(bundledNewNotes) @@ -2903,7 +2894,7 @@ class LocalCacheFlow { } fun removedNote(newNote: Note) { - bundler.invalidateList(newNote) { bundledNewNotes -> + bundler2.invalidateList(newNote) { bundledNewNotes -> _deletedEventBundles.emit(bundledNewNotes) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt index a01ee756bb..b2410ce850 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/FeedContentState.kt @@ -126,6 +126,13 @@ class FeedContentState( } } + fun deleteFromFeed(deletedNotes: Set) { + val feed = _feedContent.value + if (feed is FeedState.Loaded) { + updateFeed((feed.feed.value.list - deletedNotes).toImmutableList()) + } + } + fun refreshFromOldState(newItems: Set) { val oldNotesState = _feedContent.value if (localFilter is AdditiveFeedFilter && lastFeedKey == localFilter.feedKey()) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt index 4fffbc7b57..373a6f1d3d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/userSuggestions/ShowUserSuggestionList.kt @@ -46,7 +46,10 @@ import com.vitorpamplona.amethyst.ui.note.AboutDisplay import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.AnimateOnNewSearch import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch @Composable fun ShowUserSuggestionList( @@ -62,8 +65,15 @@ fun ShowUserSuggestionList( AnimateOnNewSearch(userSuggestions, listState) LaunchedEffect(Unit) { - LocalCache.live.newEventBundles.collect { - userSuggestions.invalidateData() + launch(Dispatchers.Default) { + LocalCache.live.newEventBundles.collect { + userSuggestions.invalidateData() + } + } + launch(Dispatchers.Default) { + LocalCache.live.deletedEventBundles.collect { + userSuggestions.invalidateData() + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index d71826742f..62ad0b4d3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -55,6 +55,13 @@ abstract class FeedViewModel( feedState.updateFeedWith(newNotes) } } + + viewModelScope.launch(Dispatchers.Default) { + LocalCache.live.deletedEventBundles.collect { newNotes -> + Log.d("Rendering Metrics", "Delete from feeds: ${this@FeedViewModel.javaClass.simpleName} with ${newNotes.size}") + feedState.deleteFromFeed(newNotes) + } + } } override fun onCleared() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 45e7da9b5b..64d0813cba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -106,6 +106,13 @@ open class UserFeedViewModel( invalidateData() } } + + viewModelScope.launch(Dispatchers.Default) { + LocalCache.live.deletedEventBundles.collect { newNotes -> + Log.d("Rendering Metrics", "Delete from feeds: ${this@UserFeedViewModel.javaClass.simpleName} with ${newNotes.size}") + invalidateData() + } + } } override fun onCleared() { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index b12107f36c..75bac2b2bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -198,7 +198,8 @@ class DecryptAndIndexProcessor( is DraftEvent -> { // Avoid decrypting over and over again if the event already exist. if (event.pubKey == account.signer.pubKey) { - deindexDraftAsRealEvent(eventNote) + // already deindexed by LocalCache + // deindexDraftAsRealEvent(eventNote) } } @@ -303,14 +304,6 @@ class DecryptAndIndexProcessor( } } - fun deindexDraftAsRealEvent(draftEventWrap: Note) { - draftEventWrap.replyTo?.forEach { it.removeReply(draftEventWrap) } - draftEventWrap.replyTo = null - - draftEventWrap.inChatroom?.removeMessageSync(draftEventWrap) - draftEventWrap.inChannel?.removeNote(draftEventWrap) - } - suspend fun runNew(newNotes: Set) { try { newNotes.forEach { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt index 77b8e7e560..b7769fb46a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt @@ -87,11 +87,12 @@ open class LnZapFeedViewModel( init { Log.d("Init", "${this.javaClass.simpleName}") viewModelScope.launch(Dispatchers.IO) { - checkNotInMainThread() - LocalCache.live.newEventBundles.collect { newNotes -> - checkNotInMainThread() - + invalidateData() + } + } + viewModelScope.launch(Dispatchers.IO) { + LocalCache.live.deletedEventBundles.collect { newNotes -> invalidateData() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt index c2f9ae4bc7..adc9244d6a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchScreen.kt @@ -77,7 +77,9 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.StdTopPadding import com.vitorpamplona.amethyst.ui.theme.placeholderText +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.launch @Composable fun SearchScreen( @@ -142,9 +144,19 @@ private fun SearchBar( TextSearchDataSourceSubscription(searchBarViewModel, accountViewModel) LaunchedEffect(Unit) { - LocalCache.live.newEventBundles.collect { - if (searchBarViewModel.isSearchingFun()) { - searchBarViewModel.invalidateData() + launch(Dispatchers.Default) { + LocalCache.live.newEventBundles.collect { + if (searchBarViewModel.isSearchingFun()) { + searchBarViewModel.invalidateData() + } + } + } + + launch(Dispatchers.Default) { + LocalCache.live.deletedEventBundles.collect { + if (searchBarViewModel.isSearchingFun()) { + searchBarViewModel.invalidateData() + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt index a98bd98ef7..607d494b9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/StringFeedViewModel.kt @@ -105,6 +105,12 @@ open class StringFeedViewModel( invalidateData() } } + viewModelScope.launch(Dispatchers.Default) { + LocalCache.live.deletedEventBundles.collect { newNotes -> + Log.d("Rendering Metrics", "Update feeds: ${this@StringFeedViewModel.javaClass.simpleName} with ${newNotes.size}") + invalidateData() + } + } } override fun onCleared() { From 6b946a57c218163d3ac369656521ab481aa7d38b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Aug 2025 16:09:29 -0400 Subject: [PATCH 183/185] Moves the exceptions to our own signing exceptions --- .../quartz/nip01Core/signers/NostrSignerInternal.kt | 5 ++--- .../quartz/nip01Core/signers/caches/DecryptCache.kt | 2 +- .../quartz/nip57Zaps/PrivateZapRequestBuilder.kt | 8 ++++---- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt index 6c8992da67..a9fc14f093 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerInternal.kt @@ -38,11 +38,10 @@ class NostrSignerInternal( inline fun runWrapErrors(action: () -> T): T = try { action() - } catch (e: SignerExceptions) { - throw e } catch (e: Exception) { if (e is CancellationException) throw e - throw SignerExceptions.CouldNotPerformException("Could not sign event.", e) + if (e is SignerExceptions) throw e + throw SignerExceptions.CouldNotPerformException("Could not perform the operation", e) } override suspend fun sign( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/DecryptCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/DecryptCache.kt index 328a73a53b..eb84002495 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/DecryptCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/caches/DecryptCache.kt @@ -62,7 +62,7 @@ abstract class DecryptCache( // User has did not reply to the approval request. Ignore until later time. cache = CacheResults.CanTryAgain(TimeUtils.tenSecondsFromNow()) } catch (e: SignerExceptions.CouldNotPerformException) { - Log.w("DecryptCache", "CouldNotPerformException", e) + // Log.w("DecryptCache", "CouldNotPerformException", e) // Decryption failed. This key might not be able to decrypt anything. Don't try again. cache = CacheResults.DontTryAgain() } catch (e: SignerExceptions.SignerNotFoundException) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt index 332f8031c1..86d4ba75f4 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip57Zaps/PrivateZapRequestBuilder.kt @@ -101,7 +101,7 @@ class PrivateZapRequestBuilder { event.createdAt, ) } else { - throw IllegalArgumentException("Couldn't find a secret to use. The private zap is neither a post nor an author zap") + throw SignerExceptions.CouldNotPerformException("Couldn't find a secret to use. The private zap is neither a post nor an author zap") } try { @@ -112,13 +112,13 @@ class PrivateZapRequestBuilder { // the sender is logged in. decryptAnonTag(event.getAnonTag(), myPrivateKeyForThisEvent, altPubkeyToUse) } else { - throw IllegalArgumentException("This private zap cannot be decrypted by this key.") + throw SignerExceptions.CouldNotPerformException("This private zap cannot be decrypted by this key.") } } else { - throw IllegalArgumentException("Recipient pubkey not found.") + throw SignerExceptions.CouldNotPerformException("Recipient pubkey not found.") } } catch (e: Exception) { - throw IllegalArgumentException("Failed to create pubkey for ZapRequest ${event.id}. ${e.message}") + throw SignerExceptions.CouldNotPerformException("Failed to create pubkey for ZapRequest ${event.id}. ${e.message}") } } From eed4350bcabfb91a706883802fe533277bdafc9f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Aug 2025 16:10:19 -0400 Subject: [PATCH 184/185] Simplifying badge composables --- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 24 +-- .../profile/header/badges/DisplayBadges.kt | 145 ++++++++---------- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 3 + amethyst/src/main/res/values/strings.xml | 1 + 4 files changed, 78 insertions(+), 95 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index a834331c41..570c04cb04 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.ProduceStateScope import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -43,9 +44,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext @Composable @@ -94,20 +93,13 @@ fun LoadAddressableNote( accountViewModel: AccountViewModel, content: @Composable (AddressableNote?) -> Unit, ) { - var note by - remember(address) { - mutableStateOf(accountViewModel.getAddressableNoteIfExists(address)) - } - - if (note == null) { - LaunchedEffect(key1 = address) { - val newNote = - withContext(Dispatchers.IO) { - accountViewModel.getOrCreateAddressableNote(address) - } - if (note != newNote) { - note = newNote - } + val note by produceState( + accountViewModel.getAddressableNoteIfExists(address), + address, + ) { + val newNote = accountViewModel.getOrCreateAddressableNote(address) + if (newNote != value) { + value = newNote } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt index 47d054a68c..0311015e86 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt @@ -20,25 +20,18 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges +import android.R.attr.onClick import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote @@ -46,23 +39,24 @@ import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMap import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav.nav import com.vitorpamplona.amethyst.ui.navigation.navs.INav -import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.BadgePictureModifier +import com.vitorpamplona.amethyst.ui.theme.Size35Modifier import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext @Composable fun DisplayBadges( @@ -114,17 +108,20 @@ private fun LoadAndRenderBadge( accountViewModel: AccountViewModel, nav: INav, ) { - var baseNote by remember(badgeAwardEvent) { mutableStateOf(LocalCache.getNoteIfExists(badgeAwardEvent)) } - - LaunchedEffect(key1 = badgeAwardEvent) { - if (baseNote == null) { - withContext(Dispatchers.IO) { - baseNote = LocalCache.checkGetOrCreateNote(badgeAwardEvent) + val baseNote = + produceState( + LocalCache.getNoteIfExists(badgeAwardEvent), + badgeAwardEvent, + ) { + val newValue = LocalCache.checkGetOrCreateNote(badgeAwardEvent) + if (newValue != value) { + value = newValue } } - } - baseNote?.let { ObserveAndRenderBadge(it, accountViewModel, nav) } + baseNote.value?.let { + ObserveAndRenderBadge(it, accountViewModel, nav) + } } @Composable @@ -133,90 +130,80 @@ private fun ObserveAndRenderBadge( accountViewModel: AccountViewModel, nav: INav, ) { - val badgeAwardState by observeNote(it, accountViewModel) - val baseBadgeDefinition = badgeAwardState.note.replyTo?.firstOrNull() - baseBadgeDefinition?.let { BadgeThumb(it, accountViewModel, nav, Size35dp) } -} - -@Composable -fun BadgeThumb( - note: Note, - accountViewModel: AccountViewModel, - nav: INav, - size: Dp, - pictureModifier: Modifier = Modifier, -) { - BadgeThumb(note, accountViewModel, size, pictureModifier) { nav.nav(Route.Note(note.idHex)) } + val badgeAwardState by observeNoteEvent(it, accountViewModel) + val badgeDefinitionId = badgeAwardState?.awardDefinition()?.firstOrNull() + if (badgeDefinitionId != null) { + LoadAddressableNote(badgeDefinitionId, accountViewModel) { badgeDefNote -> + badgeDefNote?.let { + BadgeThumb(it, accountViewModel, nav) + } + } + } } @Composable fun BadgeThumb( baseNote: Note, accountViewModel: AccountViewModel, - size: Dp, - pictureModifier: Modifier = Modifier, - onClick: ((String) -> Unit)? = null, + nav: INav, ) { Box( - remember { - Modifier - .width(size) - .height(size) - }, + modifier = + Size35Modifier.clickable( + onClick = { + nav.nav { + routeFor(baseNote, accountViewModel.account) + } + }, + ), ) { - WatchAndRenderBadgeImage(baseNote, size, pictureModifier, accountViewModel, onClick) + WatchAndRenderBadgeImage(baseNote, accountViewModel) } } @Composable private fun WatchAndRenderBadgeImage( baseNote: Note, - size: Dp, - pictureModifier: Modifier, accountViewModel: AccountViewModel, - onClick: ((String) -> Unit)?, ) { - val noteState by observeNote(baseNote, accountViewModel) - val eventId = remember(noteState) { noteState?.note?.idHex } ?: return - val image by - remember(noteState) { - derivedStateOf { - val event = noteState.note.event as? BadgeDefinitionEvent + val event by observeNoteEvent(baseNote, accountViewModel) + + event?.let { + val image = + remember(event) { event?.thumb()?.ifBlank { null } ?: event?.image()?.ifBlank { null } } + RenderBadgeImage(it.id, it.name(), image, accountViewModel) + } +} + +@Composable +private fun RenderBadgeImage( + id: String, + name: String?, + image: String?, + accountViewModel: AccountViewModel, +) { + val description = + if (name != null) { + stringRes(id = R.string.badge_award_image_for, name) + } else { + stringRes(id = R.string.badge_award_image) } if (image == null) { RobohashAsyncImage( - robot = "authornotfound", - contentDescription = stringRes(R.string.unknown_author), - modifier = - remember { - pictureModifier - .width(size) - .height(size) - }, + robot = "badgenotfound", + contentDescription = description, + modifier = BadgePictureModifier, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, ) } else { RobohashFallbackAsyncImage( - robot = eventId, - model = image!!, - contentDescription = stringRes(id = R.string.profile_image), - modifier = - remember { - pictureModifier - .width(size) - .height(size) - .clip(shape = CutCornerShape(20)) - .run { - if (onClick != null) { - this.clickable(onClick = { onClick(eventId) }) - } else { - this - } - } - }, + robot = id, + model = image, + contentDescription = description, + modifier = BadgePictureModifier, loadProfilePicture = accountViewModel.settings.showProfilePictures.value, loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index d5bbcb764d..272519c0c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.CutCornerShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Shapes @@ -344,3 +345,5 @@ val SimpleImage35Modifier = Modifier.size(Size34dp).clip(shape = CircleShape) val SimpleImageBorder = Modifier.fillMaxSize().clip(QuoteBorder) val SimpleHeaderImage = Modifier.fillMaxWidth().heightIn(max = 200.dp) + +val BadgePictureModifier = Modifier.size(35.dp).clip(shape = CutCornerShape(20)) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index b6517ec190..fb6d27b6fb 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -311,6 +311,7 @@ Error "Created by %1$s" "Badge award image for %1$s" + "Badge award image You Received a new Badge Award Badge award granted to Copied note text to clipboard From 340760513f278bef03b1eeb8c1fb3487dfca11a4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 1 Aug 2025 16:11:24 -0400 Subject: [PATCH 185/185] Better handling index cleanup when draft events get deleted --- .../vitorpamplona/amethyst/model/Channel.kt | 36 ++++++++++++------- .../amethyst/model/LocalCache.kt | 23 +++--------- .../com/vitorpamplona/amethyst/model/Note.kt | 30 +++++++++++++--- .../amethyst/model/privateChats/Chatroom.kt | 9 ++++- .../loggedIn/DecryptAndIndexProcessor.kt | 18 ++-------- 5 files changed, 65 insertions(+), 51 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt index 9ad0c49665..153f1cd730 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt @@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.utils.LargeCache import kotlinx.coroutines.flow.MutableStateFlow @Stable -abstract class Channel { +abstract class Channel : NotesGatherer { val notes = LargeCache() var lastNote: Note? = null @@ -68,21 +68,33 @@ abstract class Channel { note: Note, relay: NormalizedRelayUrl? = null, ) { - notes.put(note.idHex, note) + if (!notes.containsKey(note.idHex)) { + notes.put(note.idHex, note) + note.addGatherer(this) - if ((note.createdAt() ?: 0) > (lastNote?.createdAt() ?: 0)) { - lastNote = note + if ((note.createdAt() ?: 0) > (lastNote?.createdAt() ?: 0)) { + lastNote = note + } + + if (relay != null) { + addRelay(relay) + } + + flowSet?.notes?.invalidateData() } - - if (relay != null) { - addRelay(relay) - } - - flowSet?.notes?.invalidateData() } - fun removeNote(note: Note) { - notes.remove(note.idHex) + override fun removeNote(note: Note) { + if (notes.containsKey(note.idHex)) { + notes.remove(note.idHex) + note.removeGatherer(this) + + if (note == lastNote) { + lastNote = notes.values().sortedWith(DefaultFeedOrder).firstOrNull() + } + + flowSet?.notes?.invalidateData() + } } fun pruneOldMessages(): Set { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 55cdc78300..2d53efc172 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -347,8 +347,6 @@ object LocalCache : ILocalCache { } fun checkGetOrCreateNote(etag: ETag): Note? { - checkNotInMainThread() - if (isValidHex(etag.eventId)) { return getOrCreateNote(etag) } @@ -356,8 +354,6 @@ object LocalCache : ILocalCache { } fun checkGetOrCreateNote(key: String): Note? { - checkNotInMainThread() - if (ATag.isATag(key)) { return checkGetOrCreateAddressableNote(key) } @@ -1322,16 +1318,10 @@ object LocalCache : ILocalCache { // Counts the replies deleteNote.replyTo?.forEach { masterNote -> - masterNote.removeReply(deleteNote) - masterNote.removeBoost(deleteNote) - masterNote.removeReaction(deleteNote) - masterNote.removeZap(deleteNote) - masterNote.removeZapPayment(deleteNote) - masterNote.removeReport(deleteNote) + masterNote.removeNote(deleteNote) } - deleteNote.inChatroom?.removeMessageSync(deleteNote) - deleteNote.inChannel?.removeNote(deleteNote) + deleteNote.inGatherers?.forEach { it.removeNote(deleteNote) } getAnyChannel(deleteNote)?.removeNote(deleteNote) @@ -2406,15 +2396,10 @@ object LocalCache : ILocalCache { private fun removeFromCache(note: Note) { note.replyTo?.forEach { masterNote -> - masterNote.removeReply(note) - masterNote.removeBoost(note) - masterNote.removeReaction(note) - masterNote.removeZap(note) - masterNote.removeReport(note) + masterNote.removeNote(note) } - note.inChatroom?.removeMessageSync(note) - note.inChannel?.removeNote(note) + note.inGatherers?.forEach { it.removeNote(note) } val noteEvent = note.event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 7e420a4e82..03158d3eec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState -import com.vitorpamplona.amethyst.model.privateChats.Chatroom import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import com.vitorpamplona.amethyst.service.replace @@ -78,6 +77,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flatMapLatest import java.math.BigDecimal +interface NotesGatherer { + fun removeNote(note: Note) +} + @Stable class AddressableNote( val address: Address, @@ -125,14 +128,33 @@ class AddressableNote( @Stable open class Note( val idHex: String, -) { +) : NotesGatherer { // These fields are only available after the Text Note event is received. // They are immutable after that. var event: Event? = null var author: User? = null var replyTo: List? = null - var inChannel: Channel? = null - var inChatroom: Chatroom? = null + + var inGatherers: List? = null + + fun inGatherers() = inGatherers ?: listOf().also { inGatherers = it } + + fun addGatherer(gatherer: NotesGatherer) { + inGatherers = inGatherers() + gatherer + } + + fun removeGatherer(gatherer: NotesGatherer) { + inGatherers = inGatherers() - gatherer + } + + override fun removeNote(note: Note) { + removeReply(note) + removeBoost(note) + removeReaction(note) + removeZap(note) + removeZapPayment(note) + removeReport(note) + } // These fields are updated every time an event related to this note is received. var replies = listOf() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index d562e8ce7f..969e84bb4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.model.privateChats import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.NotesGatherer import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -31,7 +32,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.coroutines.flow.MutableStateFlow @Stable -class Chatroom { +class Chatroom : NotesGatherer { var activeSenders: Set = setOf() var messages: Set = setOf() var subject = MutableStateFlow(null) @@ -39,10 +40,15 @@ class Chatroom { var ownerSentMessage: Boolean = false var lastMessage: Note? = null + override fun removeNote(note: Note) { + removeMessageSync(note) + } + @Synchronized fun addMessageSync(msg: Note): Boolean { if (msg !in messages) { messages = messages + msg + msg.addGatherer(this) msg.author?.let { author -> if (author !in activeSenders) { @@ -70,6 +76,7 @@ class Chatroom { fun removeMessageSync(msg: Note): Boolean { if (msg in messages) { messages = messages - msg + msg.removeGatherer(this) messages .filter { it.event?.subject() != null } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 75bac2b2bb..b7ef28cb12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -256,49 +256,37 @@ class DecryptAndIndexProcessor( is PrivateDmEvent -> { if (rumor.canDecrypt(account.signer)) { val talkingWith = rumor.chatroomKey(account.signer.pubKey) - val chatroom = account.chatroomList.getOrCreatePrivateChatroom(talkingWith) - if (chatroom.addMessageSync(draftEventWrap)) { - draftEventWrap.inChatroom = chatroom - } + account.chatroomList.addMessage(talkingWith, draftEventWrap) } } is ChatMessageEvent -> { if (rumor.isIncluded(account.signer.pubKey)) { val key = rumor.chatroomKey(account.signer.pubKey) - val chatroom = account.chatroomList.getOrCreatePrivateChatroom(key) - if (chatroom.addMessageSync(draftEventWrap)) { - draftEventWrap.inChatroom = chatroom - } + account.chatroomList.addMessage(key, draftEventWrap) } } is ChatMessageEncryptedFileHeaderEvent -> { if (rumor.isIncluded(account.signer.pubKey)) { val key = rumor.chatroomKey(account.signer.pubKey) - val chatroom = account.chatroomList.getOrCreatePrivateChatroom(key) - if (chatroom.addMessageSync(draftEventWrap)) { - draftEventWrap.inChatroom = chatroom - } + account.chatroomList.addMessage(key, draftEventWrap) } } is EphemeralChatEvent -> { rumor.roomId()?.let { val channel = cache.getOrCreateEphemeralChannel(it) channel.addNote(draftEventWrap, null) - draftEventWrap.inChannel = channel } } is ChannelMessageEvent -> { rumor.channelId()?.let { channelId -> val channel = cache.checkGetOrCreatePublicChatChannel(channelId) channel?.addNote(draftEventWrap, null) - draftEventWrap.inChannel = channel } } is LiveActivitiesChatMessageEvent -> { rumor.activityAddress()?.let { channelId -> val channel = cache.getOrCreateLiveChannel(channelId) channel.addNote(draftEventWrap, null) - draftEventWrap.inChannel = channel } } }